openapi: 3.1.0
info:
  title: StudentPay Provider Checkout API
  version: 1.0.0
  summary: Education-provider enrolment, payment-plan establishment, and agreement confirmation
  description: |
    StudentPay Provider Checkout API v1 supports education-provider enrolment wizards:

    1. Create a provider checkout (Contact, Opportunity/payment plan, optional DDA)
    2. Complete direct-debit authorisation via the returned setup URL
    3. Confirm enrolment (agreement acceptance, PDF generation, Opportunity stage update)
    4. Retrieve checkout status

    Legacy routes (`/api/provider-checkout`, `/api/provider-checkout-confirm`) remain
    available for existing OnFit integrations. New providers should use `/v1`.

    Human-readable guides and an interactive API Reference are published from the
    `docs-site/` package (target host: `https://docs.studentpay.com.au`).
  contact:
    name: StudentPay
    email: dsherwood@studentpay.com.au
    url: https://www.studentpay.com.au

servers:
  - url: https://sandbox-api.studentpay.com.au
    description: Sandbox
  - url: https://api.studentpay.com.au
    description: Production

tags:
  - name: Environment
    description: Public sandbox/production discovery
  - name: Provider Checkouts
    description: Enrolment checkout lifecycle for education providers
  - name: Errors
    description: Standard error envelope

security:
  - ProviderApiKey: []

paths:
  /v1/environment:
    get:
      tags: [Environment]
      operationId: getEnvironment
      summary: Retrieve active environment metadata
      description: |
        Returns non-secret metadata for the active StudentPay environment.
        Use this to confirm sandbox vs production before integrating.
      security: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Environment metadata
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EnvironmentResponse"
              example:
                success: true
                environment: sandbox
                api_base_url: https://sandbox-api.studentpay.com.au
                v1_base_url: https://sandbox-api.studentpay.com.au/v1
                pinch_mode: test
                request_id: req_3f9c2a1b8e7d6c5a4b3d2e1f

  /v1/provider-checkouts:
    post:
      tags: [Provider Checkouts]
      operationId: createProviderCheckout
      summary: Create a provider checkout
      description: |
        Creates or reuses Salesforce Contact and Opportunity records for a provider
        enrolment, optionally creates a Direct Debit Authorisation, and returns a
        DDA setup URL when a payment plan requires bank authority.

        Idempotency: send `Idempotency-Key`, or rely on the default
        `{provider_code}:{provider_order_id}` key. Repeated create calls return the
        original checkout rather than creating duplicate Opportunities.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/RequestId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCheckoutRequest"
            examples:
              paymentPlan:
                summary: Interest-free payment plan
                value:
                  provider:
                    provider_code: ONFIT
                    provider_order_id: ONFIT-ENROLMENT-12345
                    provider_name: OnFit Training College
                    provider_account_id: 001XXXXXXXXXXXX
                  student:
                    first_name: Alex
                    last_name: Student
                    email: alex.student@example.com
                    mobile: "0400000000"
                    address:
                      street_address: 1 Example Street
                      suburb: Brisbane
                      state: QLD
                      postcode: "4000"
                      country: Australia
                  course:
                    course_name: Certificate IV in Fitness
                  pricing:
                    course_price: 2990
                    amount_to_finance: 2490
                    upfront_payment: 500
                  plan:
                    payment_type: interest_free_payment_plan
                    payment_frequency: Fortnightly
                    number_of_instalments: 20
                    instalment_amount: 124.5
                    first_payment_date: "2026-08-18"
                  deposit:
                    confirmed: true
                    amount: 500
      responses:
        "200":
          description: Checkout created or idempotent replay
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateCheckoutResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/provider-checkouts/{checkoutId}:
    get:
      tags: [Provider Checkouts]
      operationId: getProviderCheckout
      summary: Retrieve checkout status
      parameters:
        - $ref: "#/components/parameters/CheckoutId"
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Checkout found
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GetCheckoutResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/provider-checkouts/{checkoutId}/confirm:
    post:
      tags: [Provider Checkouts]
      operationId: confirmProviderCheckout
      summary: Confirm an enrolment
      description: |
        Confirms the enrolment after direct-debit authorisation and student
        declarations. Creates a Payment Plan Agreement, records acceptance
        evidence, generates a PDF, and moves the Opportunity to
        `Payment Plan Signed`.
      parameters:
        - $ref: "#/components/parameters/CheckoutId"
        - $ref: "#/components/parameters/RequestId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ConfirmCheckoutRequest"
            examples:
              confirm:
                value:
                  provider:
                    provider_code: ONFIT
                    provider_order_id: ONFIT-ENROLMENT-12345
                  checkout:
                    checkout_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
                    opportunity_id: 006XXXXXXXXXXXX
                    dda_id: a0XXXXXXXXXXXXX
                  payment:
                    payment_method: direct_debit
                    first_payment_date: "2026-08-18"
                    deposit_confirmed: true
                  declarations:
                    payment_plan_accepted: true
                    information_confirmed: true
                    privacy_consent_accepted: true
      responses:
        "200":
          description: Enrolment confirmed (or already confirmed)
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConfirmCheckoutResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalError"

components:
  securitySchemes:
    ProviderApiKey:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: |
        Per-provider API key supplied as `Authorization: Bearer <key>`.
        The key must match `provider.provider_code` in the request body.

  parameters:
    CheckoutId:
      name: checkoutId
      in: path
      required: true
      schema:
        type: string
        example: ONFIT-1720000000000-AB12CD34
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        maxLength: 255
        example: ONFIT-ENROLMENT-12345
      description: |
        Optional. When omitted, StudentPay uses
        `{provider_code}:{provider_order_id}` as the create-checkout key.
    RequestId:
      name: X-Request-Id
      in: header
      required: false
      schema:
        type: string
        example: req_3f9c2a1b8e7d6c5a4b3d2e1f
      description: Optional caller-supplied correlation ID. StudentPay generates one when absent.

  headers:
    XRequestId:
      description: Correlation ID for this request
      schema:
        type: string

  schemas:
    EnvironmentResponse:
      type: object
      required: [success, environment, api_base_url, v1_base_url, pinch_mode, request_id]
      properties:
        success:
          type: boolean
          const: true
        platform:
          type: string
          example: StudentPay Developer Platform
        environment:
          type: string
          enum: [sandbox, production]
        source:
          type: string
        host:
          type: [string, "null"]
        api_base_url:
          type: string
          format: uri
        v1_base_url:
          type: string
          format: uri
        pinch_mode:
          type: string
          enum: [test, live]
        salesforce_login_url_hint:
          type: string
        separated_credentials:
          type: array
          items:
            type: string
        readiness:
          type: object
          properties:
            ready_for_api_calls:
              type: boolean
            missing_required:
              type: array
              items:
                type: string
            missing_optional:
              type: array
              items:
                type: string
            configured_provider_api_keys:
              type: array
              items:
                type: string
            blockers:
              type: array
              items:
                type: string
        notes:
          type: array
          items:
            type: string
        request_id:
          type: string

    ErrorObject:
      type: object
      required: [code, message, request_id]
      properties:
        code:
          type: string
          example: INVALID_PROVIDER_ORDER_ID
        message:
          type: string
        field:
          type: [string, "null"]
          example: provider.provider_order_id
        details:
          type: object
          additionalProperties: true
        request_id:
          type: string

    ErrorResponse:
      type: object
      required: [success, error, request_id]
      properties:
        success:
          type: boolean
          const: false
        error:
          $ref: "#/components/schemas/ErrorObject"
        request_id:
          type: string

    Address:
      type: object
      properties:
        street_address:
          type: string
        suburb:
          type: string
        state:
          type: string
        postcode:
          type: string
        country:
          type: string
          default: Australia

    CreateCheckoutRequest:
      type: object
      required: [provider, student, course, pricing, plan]
      properties:
        provider:
          type: object
          required: [provider_code, provider_order_id]
          properties:
            provider_code:
              type: string
              enum: [OCA, ACADEMY, ONFIT]
            provider_order_id:
              type: string
            provider_name:
              type: string
            provider_account_id:
              type: string
              description: Salesforce Account Id for the education provider
            success_url:
              type: string
              format: uri
            cancel_url:
              type: string
              format: uri
        student:
          type: object
          required: [first_name, last_name, email]
          properties:
            first_name:
              type: string
            last_name:
              type: string
            email:
              type: string
              format: email
            mobile:
              type: string
            address:
              $ref: "#/components/schemas/Address"
        course:
          type: object
          required: [course_name]
          properties:
            course_name:
              type: string
            displayed_price:
              type: number
        pricing:
          type: object
          required: [course_price, amount_to_finance]
          properties:
            course_price:
              type: number
              exclusiveMinimum: 0
            amount_to_finance:
              type: number
              exclusiveMinimum: 0
            upfront_payment:
              type: number
        plan:
          type: object
          required:
            - payment_type
            - payment_frequency
            - number_of_instalments
            - instalment_amount
            - first_payment_date
          properties:
            payment_type:
              type: string
              example: interest_free_payment_plan
            payment_frequency:
              type: string
              example: Fortnightly
            number_of_instalments:
              type: number
              exclusiveMinimum: 0
            instalment_amount:
              type: number
              exclusiveMinimum: 0
            first_payment_date:
              type: string
              format: date
        deposit:
          type: object
          properties:
            confirmed:
              type: boolean
            amount:
              type: number
            amount_cents:
              type: number
        card_payment:
          type: object
          additionalProperties: true
        checkout:
          type: object
          properties:
            checkout_id:
              type: string
            source_url:
              type: string
            success_url:
              type: string
            cancel_url:
              type: string
        metadata:
          type: object
          additionalProperties: true

    CreateCheckoutResponse:
      type: object
      required: [success, provider, records, checkout, request_id]
      properties:
        success:
          type: boolean
          const: true
        request_id:
          type: string
        idempotent_replay:
          type: boolean
        idempotency_key:
          type: string
        provider:
          type: object
          properties:
            provider_code:
              type: string
            provider_order_id:
              type: string
            provider_name:
              type: [string, "null"]
        records:
          type: object
          properties:
            contact_id:
              type: [string, "null"]
            opportunity_id:
              type: [string, "null"]
            dda_id:
              type: [string, "null"]
        checkout:
          type: object
          properties:
            payment_type:
              type: [string, "null"]
            requires_direct_debit:
              type: boolean
            checkout_id:
              type: [string, "null"]
            status:
              type: string
        card_payment:
          type: object
          additionalProperties: true
        direct_debit:
          type: object
          properties:
            required:
              type: boolean
            dda_id:
              type: [string, "null"]
            setup_url:
              type: [string, "null"]
              description: >
                Hosted DDA setup URL. On idempotent replay, StudentPay remints a
                fresh JWT/URL while the checkout is still awaiting confirmation.
            redirect_url:
              type: [string, "null"]
            token:
              type: [string, "null"]
              description: >
                DDA setup JWT. Reminted on idempotent replay while checkout is open.
            token_expires_at:
              type: [string, "null"]

    GetCheckoutResponse:
      type: object
      required: [success, provider, checkout, records, request_id]
      properties:
        success:
          type: boolean
          const: true
        request_id:
          type: string
        provider:
          type: object
          additionalProperties: true
        checkout:
          type: object
          additionalProperties: true
        records:
          type: object
          additionalProperties: true
        direct_debit:
          type: [object, "null"]
          additionalProperties: true
        agreement:
          type: [object, "null"]
          additionalProperties: true

    ConfirmCheckoutRequest:
      type: object
      required: [provider, checkout, payment, declarations]
      properties:
        provider:
          type: object
          required: [provider_code, provider_order_id]
          properties:
            provider_code:
              type: string
            provider_order_id:
              type: string
        checkout:
          type: object
          required: [checkout_token, opportunity_id, dda_id]
          properties:
            checkout_token:
              type: string
            opportunity_id:
              type: string
            dda_id:
              type: string
            checkout_id:
              type: string
            checkout_id_path:
              type: string
              description: Also accepted via URL path `{checkoutId}`
        payment:
          type: object
          required: [payment_method, first_payment_date, deposit_confirmed]
          properties:
            payment_method:
              type: string
            first_payment_date:
              type: string
              format: date
            deposit_confirmed:
              type: boolean
              const: true
        declarations:
          type: object
          required:
            - payment_plan_accepted
            - information_confirmed
            - privacy_consent_accepted
          properties:
            payment_plan_accepted:
              type: boolean
              const: true
            information_confirmed:
              type: boolean
              const: true
            privacy_consent_accepted:
              type: boolean
              const: true
        agreements:
          type: object
          additionalProperties: true
        client_accepted_at:
          type: string
          format: date-time

    ConfirmCheckoutResponse:
      type: object
      required: [success, request_id]
      properties:
        success:
          type: boolean
          const: true
        request_id:
          type: string
        message:
          type: string
        opportunity_id:
          type: string
        dda_id:
          type: string
        checkout_id:
          type: [string, "null"]
        provider_order_id:
          type: string
        status:
          type: string
        payment_plan_agreement_id:
          type: [string, "null"]
        payment_plan_agreement_number:
          type: [string, "null"]
        payment_plan_agreement_status:
          type: [string, "null"]
        payment_plan_agreement_version:
          type: [string, "null"]
        pdf_content_document_id:
          type: [string, "null"]
        pdf_generated:
          type: boolean
        already_confirmed:
          type: boolean

  responses:
    BadRequest:
      description: Validation failed
      headers:
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            success: false
            error:
              code: VALIDATION_ERROR
              message: Invalid provider checkout payload
              field: provider.provider_order_id
              details:
                missing_fields:
                  - provider.provider_order_id
              request_id: req_3f9c2a1b8e7d6c5a4b3d2e1f
            request_id: req_3f9c2a1b8e7d6c5a4b3d2e1f
    Unauthorized:
      description: Missing or invalid credentials / checkout token
      headers:
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Forbidden:
      description: API key is not authorised for this provider
      headers:
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    NotFound:
      description: Provider or checkout not found
      headers:
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Conflict:
      description: Checkout state conflict
      headers:
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    InternalError:
      description: Unexpected server error
      headers:
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
