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

# Complete 3-D Secure after fingerprinting

> Call this only after charge returned `data.status` of `fingerprint_required`, once the device-fingerprint iframe has reported completion or timed out. No session ID is needed, because it was stored against the payment during charge.

The response has the same shape as a charge. `data.status` is `success`, `failed`, or `3ds_required` with `data.threeDsHtml` for the issuer challenge. Calling this at any other time is rejected.

When the issuer challenges the customer, the challenge frame posts back to Vestrapay's 3-D Secure callback, which then sends your parent window a message with `type` set to `vestrapay.3ds.complete`. Listen for it, then confirm with verify on your server. As a fallback, watch the `checkout.completed` event on the checkout events stream.




## OpenAPI

````yaml /openapi.yaml post /payment/checkout/charge/card/authenticate
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/charge/card/authenticate:
    post:
      tags:
        - Checkout
      summary: Complete 3-D Secure after fingerprinting
      description: >
        Call this only after charge returned `data.status` of
        `fingerprint_required`, once the device-fingerprint iframe has reported
        completion or timed out. No session ID is needed, because it was stored
        against the payment during charge.


        The response has the same shape as a charge. `data.status` is `success`,
        `failed`, or `3ds_required` with `data.threeDsHtml` for the issuer
        challenge. Calling this at any other time is rejected.


        When the issuer challenges the customer, the challenge frame posts back
        to Vestrapay's 3-D Secure callback, which then sends your parent window
        a message with `type` set to `vestrapay.3ds.complete`. Listen for it,
        then confirm with verify on your server. As a fallback, watch the
        `checkout.completed` event on the checkout events stream.
      operationId: authenticate-card
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AuthenticateCardRequest'
            example:
              browser: >-
                Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
                AppleWebKit/537.36
      responses:
        '201':
          description: Authentication result. Read `data.status`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChargeCardResponse'
              examples:
                challenge:
                  summary: Issuer challenge needed
                  value:
                    status: success
                    data:
                      status: 3ds_required
                      reference: VPY_TXN_ACME001
                      threeDsHtml: <iframe …></iframe>
                success:
                  summary: Authenticated and charged
                  value:
                    status: success
                    data:
                      status: success
                      reference: VPY_TXN_ACME001
        '401':
          $ref: '#/components/responses/CheckoutUnauthorized'
        '410':
          $ref: '#/components/responses/SessionExpired'
      security:
        - AccessCode: []
        - AccessCodeQuery: []
components:
  schemas:
    AuthenticateCardRequest:
      type: object
      description: Browser details for the 3-D Secure step. No session ID is needed.
      properties:
        browser:
          type: string
          description: The browser user agent string.
        browserDetails:
          type: object
          description: The same browser details as on charge.
          additionalProperties: true
    ChargeCardResponse:
      type: object
      required:
        - status
        - data
      description: >-
        The outer `status` is `success` whenever the call worked, even when the
        card was declined. The outcome is `data.status`.
      properties:
        status:
          type: string
          enum:
            - success
        data:
          type: object
          properties:
            status:
              type: string
              enum:
                - fingerprint_required
                - 3ds_required
                - success
                - failed
                - processing
              description: >
                The card charge outcome.


                - `fingerprint_required`: inject `fingerprintHtml` in a hidden
                iframe, then call authenticate.

                - `3ds_required`: show `threeDsHtml` in an on-page frame so the
                customer can complete the issuer challenge.

                - `processing`: the charge is in flight. Do not fulfil. Wait for
                the event or webhook, or poll verify.

                - `success`: the processor reports the payment as paid. Confirm
                with verify on your server.

                - `failed`: the charge failed. Show `message` and offer another
                card with a new session.
            reference:
              type: string
              description: The payment reference.
            message:
              type: string
              description: A message you can show to the customer. Present on failure.
            fingerprintHtml:
              type: string
              description: >-
                HTML for the hidden device-fingerprint iframe. Present when
                `status` is `fingerprint_required`.
            threeDsHtml:
              type: string
              description: >-
                HTML for the issuer challenge frame. Present when `status` is
                `3ds_required`.
    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.
  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.
    SessionExpired:
      description: >-
        The checkout session is past its 60 minutes (`session.expired`).
        Initialise a new payment and restart checkout.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            status: failed
            error: session.expired
            message: This checkout session has expired.
  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.