openapi: 3.1.0
info:
  title: StudentPay NZ Provider Checkout API
  version: 1.0.0
  summary: New Zealand education-provider enrolment, BECS NZ direct-debit setup, and checkout confirmation
  description: |
    The StudentPay NZ Provider Checkout API lets education providers and their
    software vendors enrol a student on an interest-free payment plan.

    Typical sandbox journey:

    1. Authenticate with a **sandbox** provider API key.
    2. `POST /v1/provider-checkouts` to create the checkout.
    3. Send the payer to the hosted direct-debit setup URL (browser).
    4. The payer completes GoCardless BECS NZ setup.
    5. Poll `GET /v1/provider-checkouts/{checkoutId}` until setup is complete.
    6. `POST /v1/provider-checkouts/{checkoutId}/confirm` to finish enrolment.

    **Direct debit setup complete** means the payer finished hosted GoCardless
    setup and StudentPay has the Billing Request / mandate information. The
    mandate may still be `pending_submission` or `submitted`. That is enough
    for confirmation.

    **Direct debit Authorised** means the GoCardless mandate is `active`.
    Setup complete is not Authorised.

    Confirmation does **not** collect money. After confirm, StudentPay stores
    the Payment Plan Agreement. Salesforce creates the payment schedule and
    owns later collection.

    New providers should use `/v1` only.

    ## Base URLs

    | Environment | Base URL |
    |---|---|
    | Sandbox (recommended) | `https://sandbox-api.studentpay.co.nz` |
    | Production | `https://api.studentpay.co.nz` |

    Confirm sandbox readiness (no API key):

    ```bash
    curl https://sandbox-api.studentpay.co.nz/v1/environment
    ```

    Expect `"environment": "sandbox"`, `"payment_processor": "gocardless"`,
    and `"ready_for_api_calls": true`.

    ## Authentication

    ```http
    Authorization: Bearer <YOUR_API_KEY>
    Content-Type: application/json
    ```

    Sandbox and production keys are separate and provider-specific. Keep keys
    on your server. Never embed them in browser JavaScript.

    `GET /v1` and `GET /v1/environment` do not require a key.

    ## Support

    Contact [partners@studentpay.com.au](mailto:partners@studentpay.com.au)
    for sandbox access or production keys.
  contact:
    name: StudentPay
    email: partners@studentpay.com.au
    url: https://www.studentpay.co.nz

servers:
  - url: https://sandbox-api.studentpay.co.nz
    description: Sandbox (recommended)
  - url: https://api.studentpay.co.nz
    description: Production

tags:
  - name: Discovery
    description: Public readiness and endpoint listing. No API key required.
  - name: Provider Checkouts
    description: |
      Create a checkout, retrieve status, and confirm enrolment after hosted
      BECS NZ direct-debit setup. Authenticated with a provider API key.
  - name: Errors
    description: >
      Stable success=false envelope with error.code and request_id.

security:
  - ProviderApiKey: []

paths:
  /v1:
    get:
      tags: [Discovery]
      operationId: getV1Discovery
      summary: List v1 provider endpoints
      description: |
        Public discovery document for the NZ Provider Checkout API.
        Use this when opening the API root in a browser or checking which
        provider routes are advertised.
      security: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Discovery document
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscoveryResponse"
              example:
                success: true
                platform: StudentPay NZ Developer Platform
                environment: sandbox
                api_base_url: https://sandbox-api.studentpay.co.nz
                v1_base_url: https://sandbox-api.studentpay.co.nz/v1
                payment_processor: gocardless
                payment_processor_status: configured
                documentation:
                  docs: https://docs.studentpay.co.nz
                  openapi: https://docs.studentpay.co.nz/openapi.yaml
                  reference: https://docs.studentpay.co.nz/reference/
                endpoints:
                  - method: GET
                    path: /v1/environment
                    description: Sandbox/production readiness metadata (no secrets)
                  - method: POST
                    path: /v1/provider-checkouts
                    description: Create a provider checkout
                  - method: GET
                    path: /v1/provider-checkouts/{checkoutId}
                    description: Retrieve checkout status
                  - method: POST
                    path: /v1/provider-checkouts/{checkoutId}/confirm
                    description: Confirm enrolment after DDA + declarations
                request_id: req_3f9c2a1b8e7d6c5a4b3d2e1f
        "405":
          $ref: "#/components/responses/MethodNotAllowed"

  /v1/environment:
    get:
      tags: [Discovery]
      operationId: getEnvironment
      summary: Retrieve environment readiness
      description: |
        Public, non-secret metadata for the active StudentPay NZ environment.
        Use this first in sandbox. Missing configuration is reported by
        **variable name** only — never secret values.
      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
                platform: StudentPay NZ Developer Platform
                platform_code: studentpay-nz
                country: NZ
                environment: sandbox
                api_base_url: https://sandbox-api.studentpay.co.nz
                v1_base_url: https://sandbox-api.studentpay.co.nz/v1
                payment_processor: gocardless
                payment_processor_status: configured
                readiness:
                  ready_for_api_calls: true
                  missing_required: []
                  blockers: []
                request_id: req_3f9c2a1b8e7d6c5a4b3d2e1f
        "405":
          $ref: "#/components/responses/MethodNotAllowed"

  /v1/provider-checkouts:
    post:
      tags: [Provider Checkouts]
      operationId: createProviderCheckout
      summary: Create a provider checkout
      description: |
        Creates or reuses the enrolment checkout for a provider order.

        For `plan.payment_type = interest_free_payment_plan` the API creates
        a draft direct-debit authority and returns `direct_debit.setup_url`.
        Send the **payer's browser** to that URL. Do not call setup with
        your API key.

        Create does not collect a payment and does not mark the mandate
        Authorised.

        Idempotency: send `Idempotency-Key`, or StudentPay uses
        `{provider_code}:{provider_order_id}`. A repeated create returns the
        existing checkout with `idempotent_replay: true` and may remint a
        fresh setup URL. `checkout.created` is not re-emitted on replay.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/RequestId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCheckoutRequest"
            examples:
              paymentPlan:
                summary: Sandbox interest-free payment plan
                value:
                  provider:
                    provider_code: SANDBOX_DEMO
                    provider_order_id: DEMO-ENROLMENT-1001
                  student:
                    first_name: Alex
                    last_name: Student
                    email: alex.student@example.com
                  course:
                    course_name: Example Certificate
                  pricing:
                    course_price: 2800
                    amount_to_finance: 2790
                    upfront_payment: 10
                  plan:
                    payment_type: interest_free_payment_plan
                    payment_frequency: Weekly
                    number_of_instalments: 186
                    instalment_amount: 15
                    first_payment_date: "2026-08-20"
      responses:
        "200":
          description: Checkout created or idempotent replay
          headers:
            X-Request-Id:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateCheckoutResponse"
              examples:
                created:
                  summary: New payment-plan checkout
                  value:
                    success: true
                    request_id: req_3f9c2a1b8e7d6c5a4b3d2e1f
                    idempotent_replay: false
                    idempotency_key: DEMO-ENROLMENT-1001
                    payment_processor: gocardless
                    provider:
                      provider_code: SANDBOX_DEMO
                      provider_order_id: DEMO-ENROLMENT-1001
                      provider_name: Example Provider
                    records:
                      contact_id: 003XXXXXXXXXXXX
                      opportunity_id: 006XXXXXXXXXXXX
                      dda_id: a0XXXXXXXXXXXXX
                    checkout:
                      payment_type: interest_free_payment_plan
                      requires_direct_debit: true
                      checkout_id: SANDBOX_DEMO-1720000000000-AB12CD34
                      status: direct_debit_setup_required
                    direct_debit:
                      required: true
                      setup_complete: false
                      billing_request_status: null
                      mandate_status: null
                      authorisation_status: null
                      dda_id: a0XXXXXXXXXXXXX
                      setup_url: https://sandbox-api.studentpay.co.nz/api/dd-setup?token=eyJ…
                      redirect_url: https://sandbox-api.studentpay.co.nz/api/dd-setup?token=eyJ…
                      token: eyJ…
                      token_expires_at: "2026-08-20T00:00:00.000Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "405":
          $ref: "#/components/responses/MethodNotAllowed"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/provider-checkouts/{checkoutId}:
    get:
      tags: [Provider Checkouts]
      operationId: getProviderCheckout
      summary: Retrieve checkout status
      description: |
        Returns checkout, direct-debit, and agreement status for the
        authenticated provider. Use this after hosted setup to wait until
        `direct_debit.setup_complete` is `true` before calling confirm.

        `direct_debit.authorised` is `true` only when the mandate is
        Authorised (`active`). It can remain `false` after setup complete.
      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"
              example:
                success: true
                request_id: req_3f9c2a1b8e7d6c5a4b3d2e1f
                provider:
                  provider_code: SANDBOX_DEMO
                  provider_order_id: DEMO-ENROLMENT-1001
                checkout:
                  checkout_id: SANDBOX_DEMO-1720000000000-AB12CD34
                  status: direct_debit_setup_required
                  stage_name: Prospecting
                  created_at: "2026-08-13T01:00:00.000Z"
                records:
                  opportunity_id: 006XXXXXXXXXXXX
                  contact_id: 003XXXXXXXXXXXX
                  dda_id: a0XXXXXXXXXXXXX
                  payment_plan_agreement_id: null
                direct_debit:
                  dda_id: a0XXXXXXXXXXXXX
                  required: true
                  setup_complete: true
                  billing_request_status: fulfilled
                  mandate_status: pending_submission
                  authorisation_status: Customer In Progress
                  authorised: false
                agreement: null
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "405":
          $ref: "#/components/responses/MethodNotAllowed"
        "500":
          $ref: "#/components/responses/InternalError"

  /v1/provider-checkouts/{checkoutId}/confirm:
    post:
      tags: [Provider Checkouts]
      operationId: confirmProviderCheckout
      summary: Confirm an enrolment
      description: |
        Confirms a payment-plan enrolment after **direct debit setup complete**
        and student declarations.

        Setup-complete gate (evaluated from StudentPay's stored mandate data,
        not from browser claims):

        - Billing Request status `fulfilled`
        - Mandate id present
        - Mandate status in `pending_submission`, `submitted`, or `active`

        Incomplete setup returns `409` with `DIRECT_DEBIT_SETUP_INCOMPLETE`.
        Authorised / mandate `active` is **not** required.

        Confirm generates the Payment Plan Agreement (when one does not
        already exist) and marks the checkout confirmed. It does not collect
        money and does not create payment attempts.

        A repeated confirm returns `already_confirmed: true` and does not
        create a second agreement or emit `checkout.confirmed` again.

        The path must include `{checkoutId}`. A bare
        `/v1/provider-checkouts/confirm` returns `405`.

        Use `direct_debit.token` from create as `checkout.checkout_token`.
      parameters:
        - $ref: "#/components/parameters/CheckoutId"
        - $ref: "#/components/parameters/RequestId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ConfirmCheckoutRequest"
            examples:
              confirmAfterSetupComplete:
                summary: Confirm after setup complete (sandbox)
                value:
                  provider:
                    provider_code: SANDBOX_DEMO
                    provider_order_id: DEMO-ENROLMENT-1001
                  checkout:
                    checkout_id: SANDBOX_DEMO-1720000000000-AB12CD34
                    checkout_token: eyJ…
                    opportunity_id: 006XXXXXXXXXXXX
                    dda_id: a0XXXXXXXXXXXXX
                  payment:
                    payment_method: studentpay_payment_plan
                    first_payment_date: "2026-08-20"
                    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"
              examples:
                confirmed:
                  summary: First confirmation
                  value:
                    success: true
                    request_id: req_3f9c2a1b8e7d6c5a4b3d2e1f
                    message: Your enrolment and StudentPay payment plan have been confirmed.
                    provider_code: SANDBOX_DEMO
                    provider_order_id: DEMO-ENROLMENT-1001
                    opportunity_id: 006XXXXXXXXXXXX
                    dda_id: a0XXXXXXXXXXXXX
                    checkout_id: SANDBOX_DEMO-1720000000000-AB12CD34
                    status: Confirmed
                    checkout:
                      status: confirmed
                      id: SANDBOX_DEMO-1720000000000-AB12CD34
                    enrolment:
                      status: complete
                    direct_debit:
                      setup_complete: true
                      billing_request_status: fulfilled
                      mandate_status: pending_submission
                      authorisation_status: Customer In Progress
                    agreement:
                      id: a0JXXXXXXXXXXXX
                      number: PPA-000001
                      status: Accepted
                      pdf_content_document_id: 069XXXXXXXXXXXX
                      pdf_generated: true
                    payment_plan_agreement_id: a0JXXXXXXXXXXXX
                    payment_plan_agreement_number: PPA-000001
                    pdf_generated: true
                    already_confirmed: false
        "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"
        "405":
          $ref: "#/components/responses/MethodNotAllowed"
        "500":
          $ref: "#/components/responses/InternalError"

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

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

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

  schemas:
    DiscoveryResponse:
      type: object
      required: [success, environment, api_base_url, v1_base_url, request_id]
      properties:
        success:
          type: boolean
          const: true
        platform:
          type: string
        environment:
          type: string
          enum: [sandbox, production]
        api_base_url:
          type: string
          format: uri
        v1_base_url:
          type: string
          format: uri
        payment_processor:
          type: string
          enum: [gocardless]
        payment_processor_status:
          type: string
          enum: [not_configured, configured]
        documentation:
          type: object
          additionalProperties: true
        endpoints:
          type: array
          items:
            type: object
            properties:
              method:
                type: string
              path:
                type: string
              description:
                type: string
        readiness:
          type: object
          additionalProperties: true
        request_id:
          type: string

    EnvironmentResponse:
      type: object
      required:
        - success
        - environment
        - api_base_url
        - v1_base_url
        - payment_processor
        - payment_processor_status
        - request_id
      properties:
        success:
          type: boolean
          const: true
        platform:
          type: string
          example: StudentPay NZ Developer Platform
        platform_code:
          type: string
        country:
          type: string
          example: NZ
        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
        dda_setup_base_url:
          type: string
        payment_processor:
          type: string
          enum: [gocardless]
        payment_processor_status:
          type: string
          enum: [not_configured, configured]
        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
              description: Provider codes that have a key configured. Not secret values.
            blockers:
              type: array
              items:
                type: string
            webhook_destinations_configured:
              type: boolean
            payment_processor_configured:
              type: boolean
        notes:
          type: array
          items:
            type: string
        request_id:
          type: string

    ErrorObject:
      type: object
      required: [code, message, request_id]
      properties:
        code:
          type: string
          example: VALIDATION_ERROR
        message:
          type: string
        field:
          type: [string, "null"]
        details:
          type: object
          additionalProperties: true
        billing_request_status:
          type: [string, "null"]
        mandate_status:
          type: [string, "null"]
        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
          example: New Zealand

    CreateCheckoutRequest:
      type: object
      required: [provider, student, course, pricing, plan]
      properties:
        provider:
          type: object
          required: [provider_code, provider_order_id]
          properties:
            provider_code:
              type: string
              description: |
                Must match the provider API key. Sandbox examples use
                `SANDBOX_DEMO`. Issued production codes are assigned by
                StudentPay.
              example: SANDBOX_DEMO
            provider_order_id:
              type: string
              example: DEMO-ENROLMENT-1001
            provider_name:
              type: string
            provider_account_id:
              type: string
              description: Optional education-provider account id when issued by StudentPay.
            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
              description: Full course price. Must be greater than zero.
              example: 2800
            amount_to_finance:
              type: number
              exclusiveMinimum: 0
              description: Amount collected on the instalment plan. Must be greater than zero.
              example: 2790
            upfront_payment:
              type: number
              description: Optional amount due before instalments. Not charged by this API.
              example: 10
        plan:
          type: object
          required:
            - payment_type
            - payment_frequency
            - number_of_instalments
            - instalment_amount
            - first_payment_date
          properties:
            payment_type:
              type: string
              description: Use `interest_free_payment_plan` for NZ enrolment checkouts.
              example: interest_free_payment_plan
            payment_frequency:
              type: string
              example: Weekly
            number_of_instalments:
              type: number
              exclusiveMinimum: 0
              example: 186
            instalment_amount:
              type: number
              exclusiveMinimum: 0
              example: 15
            first_payment_date:
              type: string
              format: date
              example: "2026-08-20"
        deposit:
          type: object
          properties:
            confirmed:
              type: boolean
            amount:
              type: number
        checkout:
          type: object
          properties:
            checkout_id:
              type: string
            success_url:
              type: string
            cancel_url:
              type: string
        metadata:
          type: object
          additionalProperties: true

    DirectDebitCreate:
      type: object
      properties:
        required:
          type: boolean
        setup_complete:
          type: boolean
          description: False on a newly created checkout.
        billing_request_status:
          type: [string, "null"]
        mandate_status:
          type: [string, "null"]
        authorisation_status:
          type: [string, "null"]
        dda_id:
          type: [string, "null"]
        setup_url:
          type: [string, "null"]
          description: Hosted setup URL for the payer's browser. Reminted on idempotent replay while the checkout is open.
        redirect_url:
          type: [string, "null"]
        token:
          type: [string, "null"]
          description: DDA setup JWT. Use this as `checkout.checkout_token` on confirm. Never publish a real token.
        token_expires_at:
          type: [string, "null"]

    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
        payment_processor:
          type: string
          enum: [gocardless]
        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
              example: direct_debit_setup_required
        direct_debit:
          $ref: "#/components/schemas/DirectDebitCreate"

    GetCheckoutResponse:
      type: object
      required: [success, provider, checkout, records, request_id]
      properties:
        success:
          type: boolean
          const: true
        request_id:
          type: string
        provider:
          type: object
          properties:
            provider_code:
              type: [string, "null"]
            provider_order_id:
              type: [string, "null"]
        checkout:
          type: object
          properties:
            checkout_id:
              type: [string, "null"]
            status:
              type: [string, "null"]
            stage_name:
              type: [string, "null"]
            created_at:
              type: [string, "null"]
        records:
          type: object
          properties:
            opportunity_id:
              type: [string, "null"]
            contact_id:
              type: [string, "null"]
            dda_id:
              type: [string, "null"]
            payment_plan_agreement_id:
              type: [string, "null"]
        direct_debit:
          type: [object, "null"]
          properties:
            dda_id:
              type: [string, "null"]
            required:
              type: boolean
            setup_complete:
              type: boolean
            billing_request_status:
              type: [string, "null"]
            mandate_status:
              type: [string, "null"]
            authorisation_status:
              type: [string, "null"]
            authorised:
              type: boolean
              description: True only when the DDA is Authorised (mandate active). Distinct from setup_complete.
            status:
              type: [string, "null"]
              description: Legacy alias of authorisation_status.
            processor_status:
              type: [string, "null"]
              description: Legacy alias of mandate_status.
        agreement:
          type: [object, "null"]
          properties:
            id:
              type: [string, "null"]
            number:
              type: [string, "null"]
            status:
              type: [string, "null"]
            version:
              type: [string, "null"]
            accepted_at:
              type: [string, "null"]
            pdf_content_document_id:
              type: [string, "null"]
              description: Stored file id. There is no public PDF download endpoint.

    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
              description: DDA setup JWT from create (`direct_debit.token`).
            opportunity_id:
              type: string
            dda_id:
              type: string
            checkout_id:
              type: string
        payment:
          type: object
          required: [payment_method, first_payment_date, deposit_confirmed]
          properties:
            payment_method:
              type: string
              example: studentpay_payment_plan
            first_payment_date:
              type: string
              format: date
            deposit_confirmed:
              type: boolean
              const: true
              description: Must be true. This API does not charge the deposit.
        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
        confirmed_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
        provider_code:
          type: string
        provider_order_id:
          type: string
        opportunity_id:
          type: string
        dda_id:
          type: string
        checkout_id:
          type: [string, "null"]
        status:
          type: string
        checkout:
          type: object
          properties:
            status:
              type: string
              example: confirmed
            id:
              type: [string, "null"]
        enrolment:
          type: object
          properties:
            status:
              type: string
              example: complete
        direct_debit:
          type: object
          properties:
            setup_complete:
              type: boolean
            billing_request_status:
              type: [string, "null"]
            mandate_status:
              type: [string, "null"]
            authorisation_status:
              type: [string, "null"]
        agreement:
          type: object
          properties:
            id:
              type: [string, "null"]
            number:
              type: [string, "null"]
            status:
              type: [string, "null"]
            pdf_content_document_id:
              type: [string, "null"]
            pdf_generated:
              type: boolean
        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
        agreement_already_existed:
          type: boolean
        already_confirmed:
          type: boolean
        pdf_warning:
          type: [string, "null"]

  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
              details:
                missing_fields:
                  - student.email
              request_id: req_3f9c2a1b8e7d6c5a4b3d2e1f
            request_id: req_3f9c2a1b8e7d6c5a4b3d2e1f
    Unauthorized:
      description: Missing API key or invalid / expired checkout token
      headers:
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Forbidden:
      description: |
        API key is not recognised or does not match provider_code (typically
        `INVALID_API_KEY` or `PROVIDER_KEY_MISMATCH`).

        Confirm token / identifier mismatches also return HTTP 403. On the v1
        confirm route those string errors are currently mapped to
        `INTERNAL_ERROR` while the status stays 403. Use the HTTP status and
        message, not the mapped code, for those cases.
      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. Common coded case:
        `DIRECT_DEBIT_SETUP_INCOMPLETE` when hosted setup is not finished.
      headers:
        X-Request-Id:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            success: false
            error:
              code: DIRECT_DEBIT_SETUP_INCOMPLETE
              message: The direct debit setup has not yet been completed.
              billing_request_status: pending
              mandate_status: null
              request_id: req_3f9c2a1b8e7d6c5a4b3d2e1f
            request_id: req_3f9c2a1b8e7d6c5a4b3d2e1f
    MethodNotAllowed:
      description: Unsupported HTTP method or route
      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"
