> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vestrapay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Stream checkout events

> A Server-Sent Events stream that lets a pay screen update live. Open it after you submit a charge. It is **not** wrapped in the `{ status, data }` envelope.

The stream replays stored events after `Last-Event-ID` when you send it, then emits `checkout.connected`, periodic `checkout.heartbeat` keep-alives, and progress events as they happen. It closes after the configured session timeout, which is 15 minutes by default.

| Event | Meaning |
| --- | --- |
| `checkout.connected` | The stream is open. |
| `checkout.heartbeat` | Keep-alive. Ignore it. |
| `checkout.processing` | A charge is in flight. |
| `checkout.3ds_required` | The customer must complete a 3-D Secure challenge. |
| `checkout.awaiting_transfer` | A virtual account is open and waiting for the transfer. |
| `checkout.awaiting_bank_authorization` | The customer must approve the payment at their bank. |
| `checkout.awaiting_ussd` | The customer must dial the USSD code. |
| `checkout.amount_mismatch` | A transfer of the wrong amount was received. The window is still open, so ask the customer to send the exact total. |
| `checkout.completed` | Terminal. Read `status` in the payload, then verify from your server. |
| `checkout.timeout` | The session ended without an outcome. |

The interactive playground cannot test this endpoint, because it waits for the connection to close. Use `curl -N` or an `EventSource` in the browser.




## OpenAPI

````yaml /openapi.yaml get /payment/checkout/events
openapi: 3.1.0
info:
  title: Vestrapay Payments API
  version: '1.0'
  summary: >-
    Initialise payments, collect them through hosted or custom checkout, and
    verify the result from your server.
  description: >
    The Vestrapay Payments API lets your server create a payment, lets your
    customer pay it, and lets your server confirm the outcome before it fulfils
    an order.


    ## How a payment flows


    1. **Initialise** (`POST /payment/initialize`, secret key). Your server
    creates a pending transaction and receives a `reference`, an `accessCode`
    and a hosted `checkoutUrl`.

    2. **Collect.** Send the customer to the `checkoutUrl`, or build your own
    pay screen with the **Checkout** endpoints, which use the `accessCode`
    rather than your secret key. Four channels are available: `card`,
    `bank_transfer`, `bank_payment` and `ussd`.

    3. **Confirm** (`GET /payment/verify/{reference}`, secret key). Fulfil only
    when `data.status` is `success` and `data.amount` matches the order.
    Webhooks and the browser callback are signals. Verify is the confirmation.


    ## Environments and servers


    Sandbox and live are separate environments with separate keys, data,
    services and webhook endpoints. A sandbox reference cannot be verified on
    live, and the reverse.


    | Environment | API base URL | Checkout host | Merchant dashboard |

    | --- | --- | --- | --- |

    | Sandbox | `https://api.sandbox.vestrapay.app/v1` |
    `https://checkout.sandbox.vestrapay.app` |
    `https://merchant.sandbox.vestrapay.app` |

    | Live | `https://api.vestrapay.com/v1` | `https://checkout.vestrapay.com` |
    `https://merchant.vestrapay.com` |


    Keys begin `sk_test_` for the sandbox and `sk_live_` for live. A key sent to
    the wrong host is rejected with `invalid_key_environment`. Live payments are
    refused until your business has passed KYB.


    ## Authentication


    | Scheme | Header | Used for |

    | --- | --- | --- |

    | Secret key | `x-api-key: sk_…` or `Authorization: Bearer sk_…` |
    Initialise and verify. Server-side only. Never ship it to a browser or
    mobile app. |

    | Access code | `x-access-code: …` (or the `access_code` query parameter) |
    Every Checkout endpoint. Safe to hand to the customer's browser, because it
    is scoped to one payment and expires after 60 minutes. |


    ## Amounts


    Amounts are **decimal strings in major units**. `"100.00"` is one hundred
    naira. Never send kobo, and never send a formatted value such as `"₦1,000"`.
    Compare amounts as strings or decimals, never as binary floats. NGN is the
    supported currency.


    ## Response envelope


    Every JSON response uses one of two envelopes.


    ```json

    { "status": "success", "data": { } }

    ```


    ```json

    { "status": "failed", "error": "invalid_credentials", "message": "The
    provided credentials are invalid." }

    ```


    Branch on `error`, which is stable. The `message` is for people and may
    change wording. The only unwrapped responses are the checkout event stream
    (`text/event-stream`) and webhook bodies, which are `{ "event", "data",
    "timestamp" }`. Log the `X-Request-Id` response header with every failure
    and quote it to support.


    ## Splitting a payment


    Services, sub-accounts and splits are configured in the merchant dashboard,
    and each service has a **service key**. To divide a particular payment, pass
    `distribution` on initialise as a map of service key to an absolute amount
    in the same unit as `amount`. Whatever you do not allocate settles to your
    primary settlement account. Verify returns a `split` summary once the
    payment has been allocated.


    ## Statuses at a glance


    | Where | Values |

    | --- | --- |

    | Payment (initialise, verify, status) | `pending`, `processing`, `success`,
    `failed`, `abandoned` |

    | Card charge (`data.status`) | `fingerprint_required`, `3ds_required`,
    `processing`, `success`, `failed` |

    | Transfer / USSD charge (`data.status`) | `awaiting_transfer`,
    `awaiting_ussd`, plus the processing states above |

    | Bank payment charge (`data.status`) | `processing` (with `redirectUrl`
    when the bank supplies one). The event stream may emit
    `checkout.awaiting_bank_authorization`. |


    Only `success` on verify means paid.


    ## Errors and retries


    | HTTP status | Meaning | Retry? |

    | --- | --- | --- |

    | 400 | Invalid request, or the current state does not allow it. | No. Fix
    it first. |

    | 401 | Credentials are missing, malformed, expired or wrong. | No. |

    | 403 | Authenticated but not allowed, for example KYB not approved or live
    not enabled. | No. |

    | 404 | The thing does not exist in this environment or for your account. |
    No. |

    | 409 | Conflict with existing state. | No. |

    | 410 | The session or access code has expired. | No. Initialise again. |

    | 422 | Too many attempts on this payment. | No. Initialise again. |

    | 502, 503 | A bank or processor dependency is unavailable. | Yes, with
    exponential backoff. |


    Verify is read-only, so retry it on network failure or a 5xx response.
    Initialise creates a new reference on every call, so a retried initialise
    can create a second pending payment. Carry your own order ID in `metadata`
    so that you can recognise and ignore duplicates.


    ## Not in this reference


    Managing services, sub-accounts, splits, partner users, settlements, payment
    links, webhooks and KYB is done in the merchant dashboard and is covered in
    the guides. This reference documents the Payments API only.
  contact:
    name: Vestrapay developer support
    email: tech@vestrapay.com
    url: https://vestrapay.com
servers:
  - url: https://api.sandbox.vestrapay.app/v1
    description: Sandbox. Use `sk_test_` keys. No real money moves.
  - url: https://api.vestrapay.com/v1
    description: >-
      Live. Use `sk_live_` keys. Available once KYB is approved and live mode is
      enabled.
security:
  - SecretKey: []
  - BearerAuth: []
tags:
  - name: Payments
    description: >
      Server-to-server calls made with your **secret key**.


      **Initialise** creates a pending transaction and the session your customer
      pays through. **Verify** is the canonical lookup of a payment's state and
      the only call you should trust before you fulfil an order. Both calls
      belong on your server.
  - name: Checkout
    description: >
      Customer-session calls made with the **access code** returned by
      initialise. Hosted checkout uses these for you. Call them yourself only
      when you build a custom pay screen.


      The access code is valid for 60 minutes and is scoped to a single payment.
      Choose a channel, charge it, then confirm from your server with verify.


      | Channel | Calls |

      | --- | --- |

      | Card | Create a card session, mount the processor's hosted fields,
      charge, and authenticate if 3-D Secure asks for it. |

      | Transfer | Request a one-time virtual account and show it to the
      customer. In the sandbox, simulate the outcome. |

      | Pay with bank | List banks, then start the charge for the bank the
      customer picks. |

      | USSD | Start the charge with the customer's phone number and show the
      dial string. |


      The status, verify and events calls let a pay screen show progress. They
      do not replace server-side verify.
paths:
  /payment/checkout/events:
    get:
      tags:
        - Checkout
      summary: Stream checkout events
      description: >
        A Server-Sent Events stream that lets a pay screen update live. Open it
        after you submit a charge. It is **not** wrapped in the `{ status, data
        }` envelope.


        The stream replays stored events after `Last-Event-ID` when you send it,
        then emits `checkout.connected`, periodic `checkout.heartbeat`
        keep-alives, and progress events as they happen. It closes after the
        configured session timeout, which is 15 minutes by default.


        | Event | Meaning |

        | --- | --- |

        | `checkout.connected` | The stream is open. |

        | `checkout.heartbeat` | Keep-alive. Ignore it. |

        | `checkout.processing` | A charge is in flight. |

        | `checkout.3ds_required` | The customer must complete a 3-D Secure
        challenge. |

        | `checkout.awaiting_transfer` | A virtual account is open and waiting
        for the transfer. |

        | `checkout.awaiting_bank_authorization` | The customer must approve the
        payment at their bank. |

        | `checkout.awaiting_ussd` | The customer must dial the USSD code. |

        | `checkout.amount_mismatch` | A transfer of the wrong amount was
        received. The window is still open, so ask the customer to send the
        exact total. |

        | `checkout.completed` | Terminal. Read `status` in the payload, then
        verify from your server. |

        | `checkout.timeout` | The session ended without an outcome. |


        The interactive playground cannot test this endpoint, because it waits
        for the connection to close. Use `curl -N` or an `EventSource` in the
        browser.
      operationId: checkout-events
      parameters:
        - name: Last-Event-ID
          in: header
          required: false
          description: >-
            The `id` of the last event you received, so that the stream resumes
            after it.
          schema:
            type: string
      responses:
        '200':
          description: >-
            An open `text/event-stream`. Each event has an `event` name, an
            optional `id`, and a JSON `data` line.
          content:
            text/event-stream:
              schema:
                type: string
              example: >
                event: checkout.completed

                data:
                {"status":"success","reference":"VPY_TXN_ACME001","amount":"5000.00","currency":"NGN","channel":"card"}
        '401':
          $ref: '#/components/responses/CheckoutUnauthorized'
      security:
        - AccessCode: []
        - AccessCodeQuery: []
components:
  responses:
    CheckoutUnauthorized:
      description: >
        The access code is missing, invalid or expired. Common codes are
        `missing_access_code` and `transaction_expired`. Initialise a new
        payment if the code has expired.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            status: failed
            error: missing_access_code
            message: The x-access-code header is required.
  schemas:
    ErrorEnvelope:
      type: object
      description: >-
        The envelope for every failed request. Branch on `error`, which is
        stable. Show `message` to people but never parse it.
      required:
        - status
        - error
        - message
      properties:
        status:
          type: string
          enum:
            - failed
        error:
          type: string
          description: >-
            A stable machine-readable error code, such as `invalid_credentials`
            or `payment.invalid_amount`.
          example: invalid_credentials
        message:
          type: string
          description: A human-readable explanation. Wording may change.
          example: The provided credentials are invalid.
  securitySchemes:
    SecretKey:
      type: apiKey
      in: header
      name: x-api-key
      description: >
        Your secret key. `sk_test_…` on the sandbox, `sk_live_…` on live.
        Server-side only.

        Copy it from **Settings → Developers** in the merchant dashboard.

        `Authorization: Bearer` with the same value is also accepted.
    BearerAuth:
      type: http
      scheme: bearer
      description: >-
        The same secret key sent as `Authorization: Bearer sk_…`. An alternative
        to `x-api-key`.
    AccessCode:
      type: apiKey
      in: header
      name: x-access-code
      description: >
        The `accessCode` returned by initialise. Scoped to one payment and valid
        for 60 minutes.

        Safe to give to the customer's browser.
    AccessCodeQuery:
      type: apiKey
      in: query
      name: access_code
      description: >-
        The same access code sent as the `access_code` query parameter. An
        alternative to `x-access-code`.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.