> ## 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.

# Initialise a payment

> Creates a pending transaction and returns everything you need to collect it: a generated `reference` (`VPY_TXN_…`), an `accessCode` for checkout calls, and a hosted `checkoutUrl`.

Call this from your server with your secret key each time a customer is about to pay. Redirect the customer to `checkoutUrl`, or keep `accessCode` and build your own pay screen with the Checkout endpoints. Never send the secret key to the browser.

**Fees.** The response shows what the customer would pay on each channel. `cardTotal` and `transferTotal` include the fee when the customer bears it. When `merchantBearsCost` is `true` the customer pays exactly `amount` and the fee is deducted from what you receive. The default comes from your payment settings, and you can override it per payment.

**Splitting.** Add `distribution` to allocate part of the payment to services you set up in the dashboard. Keys are service keys, values are absolute amounts, and their total must not exceed `amount`. The unallocated remainder settles to your primary settlement account. The response echoes the distribution that was applied. A distribution that names a missing or disabled service is rejected with `INVALID_SPLIT_AMOUNT` and nothing is created.

**After initialise.** Treat the payment as unpaid until verify says otherwise. The `callbackUrl` receives `status`, `reference`, `amount` and `currency` as query parameters, but that is a browser signal and can be forged or lost.




## OpenAPI

````yaml /openapi.yaml post /payment/initialize
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/initialize:
    post:
      tags:
        - Payments
      summary: Initialise a payment
      description: >
        Creates a pending transaction and returns everything you need to collect
        it: a generated `reference` (`VPY_TXN_…`), an `accessCode` for checkout
        calls, and a hosted `checkoutUrl`.


        Call this from your server with your secret key each time a customer is
        about to pay. Redirect the customer to `checkoutUrl`, or keep
        `accessCode` and build your own pay screen with the Checkout endpoints.
        Never send the secret key to the browser.


        **Fees.** The response shows what the customer would pay on each
        channel. `cardTotal` and `transferTotal` include the fee when the
        customer bears it. When `merchantBearsCost` is `true` the customer pays
        exactly `amount` and the fee is deducted from what you receive. The
        default comes from your payment settings, and you can override it per
        payment.


        **Splitting.** Add `distribution` to allocate part of the payment to
        services you set up in the dashboard. Keys are service keys, values are
        absolute amounts, and their total must not exceed `amount`. The
        unallocated remainder settles to your primary settlement account. The
        response echoes the distribution that was applied. A distribution that
        names a missing or disabled service is rejected with
        `INVALID_SPLIT_AMOUNT` and nothing is created.


        **After initialise.** Treat the payment as unpaid until verify says
        otherwise. The `callbackUrl` receives `status`, `reference`, `amount`
        and `currency` as query parameters, but that is a browser signal and can
        be forged or lost.
      operationId: initialize-payment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InitializePayment'
            examples:
              basic:
                summary: A simple payment
                value:
                  amount: '100.00'
                  email: customer@email.com
                  customerName: Jane Doe
                  callbackUrl: https://yourapp.com/paid
                  description: Order 12345
                  metadata:
                    orderId: ORD_12345
              withDistribution:
                summary: Split across two services
                description: >-
                  600.00 goes to the tuition service and 150.00 to the insurance
                  service. The remaining 250.00 settles to the primary
                  settlement account.
                value:
                  amount: '1000.00'
                  email: customer@email.com
                  callbackUrl: https://yourapp.com/pay/return
                  distribution:
                    tuition: 600
                    insurance: 150
              merchantBearsCost:
                summary: Merchant bears the fee for this payment
                value:
                  amount: '250.00'
                  email: customer@email.com
                  merchantBearsCost: true
      responses:
        '201':
          description: >-
            Transaction created. The payment is `pending` until the customer
            pays.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InitializePaymentResponse'
              examples:
                basic:
                  summary: Basic payment
                  value:
                    status: success
                    data:
                      reference: VPY_TXN_ACME001
                      accessCode: a1b2c3d4e5f6a1b2
                      checkoutUrl: https://checkout.vestrapay.com/a1b2c3d4e5f6a1b2
                      amount: '100.00'
                      cardFee: '1.50'
                      transferFee: '1.00'
                      cardTotal: '101.50'
                      transferTotal: '101.00'
                      currency: NGN
                      merchantBearsCost: false
                      channels:
                        - card
                        - bank_transfer
                        - bank_payment
                        - ussd
                      status: pending
                withDistribution:
                  summary: With a distribution echoed back
                  value:
                    status: success
                    data:
                      reference: VPY_TXN_ACME002
                      accessCode: b2c3d4e5f6a1b2c3
                      checkoutUrl: https://checkout.vestrapay.com/b2c3d4e5f6a1b2c3
                      amount: '1000.00'
                      cardFee: '15.00'
                      transferFee: '10.00'
                      cardTotal: '1015.00'
                      transferTotal: '1010.00'
                      currency: NGN
                      merchantBearsCost: false
                      channels:
                        - card
                        - bank_transfer
                        - bank_payment
                        - ussd
                      status: pending
                      distribution:
                        tuition: 600
                        insurance: 150
        '400':
          description: >-
            The request is invalid. Nothing was created. Fix the request and
            send it again.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              examples:
                invalidAmount:
                  summary: Amount is not a numeric string
                  value:
                    status: failed
                    error: payment.invalid_amount
                    message: Amount must be a numeric string such as "100.00".
                unsupportedCurrency:
                  summary: Currency is not supported
                  value:
                    status: failed
                    error: payment.unsupported_currency
                    message: Only NGN is supported.
                invalidSplitAmount:
                  summary: Distribution is invalid
                  description: >-
                    A value is not positive, the total exceeds `amount`, or a
                    service key is unknown, disabled or has no splits.
                  value:
                    status: failed
                    error: INVALID_SPLIT_AMOUNT
                    message: The amounts sum to more than the payment amount.
        '401':
          $ref: '#/components/responses/SecretKeyUnauthorized'
        '403':
          $ref: '#/components/responses/LiveNotPermitted'
        '503':
          $ref: '#/components/responses/ProviderUnavailable'
components:
  schemas:
    InitializePayment:
      type: object
      description: The body of an initialise request.
      required:
        - amount
      properties:
        amount:
          type: string
          pattern: ^\d+(\.\d{1,2})?$
          example: '100.00'
          description: >
            What you are collecting, as a decimal string in major units.
            `"100.00"` is one hundred naira. Never kobo and never a formatted
            value. When the customer bears the fee it is added on top, so this
            is the amount you expect to receive.
        currency:
          type: string
          default: NGN
          description: ISO 4217 currency code. NGN is the supported currency.
        email:
          type: string
          format: email
          description: >-
            The customer's email, used on receipts. Required for pay with
            transfer, so send it whenever you have it.
        customerName:
          type: string
          description: Shown on the hosted checkout page.
        callbackUrl:
          type: string
          format: uri
          description: >-
            Where the browser returns after checkout, with `status`,
            `reference`, `amount` and `currency` as query parameters. Use an
            HTTPS URL on your own domain. The callback is a signal only, so
            confirm with verify before you fulfil.
        description:
          type: string
          description: Free text shown on checkout, such as "Order 12345".
        metadata:
          type: object
          additionalProperties: true
          description: >-
            Any JSON object. Stored with the payment and returned on verify. Use
            it to carry your own order or customer ID.
        distribution:
          type: object
          description: >
            Allocates part of this payment to services you configured in the
            merchant dashboard. Each key is a **service key** and each value is
            an **absolute amount** in the same major unit as `amount` (a number
            with up to two decimal places).


            The total of all values must not exceed `amount`. You may allocate
            less than the whole, and often will. Each service's allocation is
            divided among its sub-accounts by its split shares, and everything
            you do not allocate settles to your primary settlement account.


            Every key must be a service that exists in this environment and is
            enabled and has at least one split. Anything else is rejected with
            `INVALID_SPLIT_AMOUNT` and no payment is created. Compute the map on
            your server. Never take allocation amounts from the browser.
          additionalProperties:
            type: number
            exclusiveMinimum: 0
          example:
            tuition: 600
            insurance: 150
        merchantBearsCost:
          type: boolean
          description: >-
            Overrides, for this payment only, who pays the fee. When `true` the
            customer pays exactly `amount` and the fee is deducted from what you
            receive. When `false` the fee is added to what the customer pays.
            Omit it to use your payment settings.
    InitializePaymentResponse:
      type: object
      required:
        - status
        - data
      properties:
        status:
          type: string
          enum:
            - success
        data:
          $ref: '#/components/schemas/InitializePaymentData'
    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.
    InitializePaymentData:
      type: object
      description: The pending payment and the session the customer pays through.
      properties:
        reference:
          type: string
          description: >-
            Vestrapay's reference for this payment, generated as `VPY_TXN_…`.
            Store it against your order. It is the key for verify.
          example: VPY_TXN_ACME001
        accessCode:
          type: string
          description: >-
            Authorises checkout calls for this payment. Valid for 60 minutes.
            Safe for the browser. Do not use it in place of your secret key.
          example: a1b2c3d4e5f6a1b2
        checkoutUrl:
          type: string
          format: uri
          description: >-
            The hosted checkout page. Redirect the customer here for the
            simplest integration.
        amount:
          type: string
          description: The amount you initialised, in major units.
          example: '100.00'
        cardFee:
          type: string
          description: The fee that applies if the customer pays by card.
        transferFee:
          type: string
          description: The fee that applies to the transfer, bank and USSD channels.
        cardTotal:
          type: string
          description: >-
            What the customer pays by card. Equals `amount` plus `cardFee` when
            the customer bears the fee, and `amount` when you do.
        transferTotal:
          type: string
          description: >-
            What the customer pays by transfer. Equals `amount` plus
            `transferFee` when the customer bears the fee, and `amount` when you
            do.
        currency:
          type: string
          example: NGN
        merchantBearsCost:
          type: boolean
          description: Whether the fee is deducted from your side for this payment.
        channels:
          type: array
          description: >-
            The channels available for this payment. A channel that is not
            enabled for your account is absent.
          items:
            type: string
            enum:
              - card
              - bank_transfer
              - bank_payment
              - ussd
        status:
          type: string
          description: The payment state. Always `pending` straight after initialise.
          example: pending
        distribution:
          type: object
          description: >-
            Echoes the distribution that was applied. Present only when you sent
            one.
          additionalProperties:
            type: number
  headers:
    XRequestId:
      description: >-
        A unique ID for this request. Log it and quote it when you contact
        support.
      schema:
        type: string
  responses:
    SecretKeyUnauthorized:
      description: >
        The secret key is missing, malformed, revoked or for the other
        environment. Common codes are `invalid_credentials`,
        `missing_credentials`, `invalid_key_format` and
        `invalid_key_environment`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            invalidCredentials:
              summary: Wrong or missing key
              value:
                status: failed
                error: invalid_credentials
                message: The provided credentials are invalid.
            wrongEnvironment:
              summary: Sandbox key on the live host, or the reverse
              value:
                status: failed
                error: invalid_key_environment
                message: This key belongs to a different environment.
    LiveNotPermitted:
      description: >
        The key is valid but live payments are not permitted yet.
        `merchant.kyb_not_approved` means KYB has not been approved.
        `live_mode_not_enabled` means live mode has not been activated, so
        contact support if KYB is already approved.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            kybNotApproved:
              summary: KYB not approved
              value:
                status: failed
                error: merchant.kyb_not_approved
                message: Your business must pass KYB before taking live payments.
            liveNotEnabled:
              summary: Live mode not enabled
              value:
                status: failed
                error: live_mode_not_enabled
                message: Live mode has not been enabled for your account.
    ProviderUnavailable:
      description: >-
        A bank or processor dependency is temporarily unavailable. Retry with
        exponential backoff.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            status: failed
            error: processor.unavailable
            message: The processor is temporarily unavailable. Try again shortly.
  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`.

````

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