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

# Checkout

> The public, unauthenticated API behind the hosted checkout page where customers pay.

Every invoice you create can be paid on a **hosted checkout page**. You share a
URL, your customer opens it, picks how they want to pay, and Hyparrow settles
the invoice and notifies you.

The checkout URL is:

```
CHECKOUT_BASE_URL/pay/{invoiceId}
```

The endpoints on this page are the **public API the hosted checkout page calls**.
They are **unauthenticated** — there are no `X-API-Key` / `X-API-Secret` headers
because the customer paying the invoice is not your API client. The base URL is
the same: `https://api.hyparrow.cloud/api/v1`.

<Note>
  **Amounts are in kobo.** `payableAmount`, `totalAmount` and friends are in the
  minor unit (`100000` = `₦1,000.00`).
</Note>

## Payment methods

The customer-facing options on the checkout page are:

| Method                          | Key            | Notes                                                                   |
| ------------------------------- | -------------- | ----------------------------------------------------------------------- |
| Bank transfer / virtual account | `bankTransfer` | Transfer the exact amount to a temporary account number. NGN only.      |
| USSD                            | `ussd`         | Dial a code generated for the customer's bank. NGN only.                |
| Card                            | `card`         | Card details are encrypted in-browser and charged via the card network. |
| OPay                            | `opay`         | Customer is redirected to OPay to authorize.                            |
| Crypto                          | `crypto`       | Pay with USDC or USDT on a supported chain (defaults to USDC on Base).  |

Which methods appear depends on the merchant's payment preferences and the
invoice currency — the NGN-only rails (bank transfer, USSD) are hidden for
non-NGN invoices. The active set is returned as `allowedPaymentMethods` from
[`GET /checkout/{invoiceId}`](#load-the-checkout).

## End-to-end flow

<Steps>
  <Step title="Customer opens the checkout page">
    The page calls `GET /checkout/{invoiceId}` to load the invoice, amounts and
    allowed payment methods.
  </Step>

  <Step title="Customer picks a method">
    Depending on the choice, the page calls `…/va`, `…/ussd`, `…/card`,
    `…/opay` or `…/crypto` to start that payment.
  </Step>

  <Step title="Customer pays">
    Transfer to the virtual account, dial the USSD code, complete the card /
    OPay redirect, or send crypto to the address.
  </Step>

  <Step title="Page polls for status">
    The page polls `GET /checkout/{invoiceId}/status` until `paid` is `true`.
  </Step>

  <Step title="Callback fires">
    Hyparrow `POST`s a `checkout.payment.*` event to the invoice's
    `checkoutCallbackUrl` (if set) as the payment progresses.
  </Step>
</Steps>

## Load the checkout

`GET /checkout/{invoiceId}`

Returns sanitized invoice data for rendering the page. Canceled invoices return
`404`.

```json theme={null}
{
  "success": true,
  "data": {
    "id": "8f1c2e34-5a6b-47c8-9d0e-112233445566",
    "title": "Website redesign",
    "invoiceNumber": "invoice-hpw-000000042",
    "currency": "NGN",
    "customerName": "Ada Obi",
    "customerEmail": "ada@example.com",
    "subTotal": 350000000,
    "taxTotal": 26250000,
    "discountTotal": 0,
    "shippingFee": 0,
    "totalAmount": 376250000,
    "serviceFeeRate": 0.025,
    "serviceFee": 500000,
    "vatRate": 0.075,
    "vatAmount": 37500,
    "payableAmount": 377037500,
    "status": "pending",
    "lineItems": [ /* ... */ ],
    "invoiceVAs": [ /* active virtual accounts */ ],
    "allowedPaymentMethods": ["bankTransfer", "card", "opay", "crypto"],
    "clientProfilePicture": "https://…"
  }
}
```

`payableAmount` is what the customer actually pays. When the invoice's
`tax_direction` is `customer`, the service fee (capped) and VAT are added on top
of `totalAmount`; when it's `merchant`, `payableAmount` equals `totalAmount` and
the fees are settled from the merchant's payout.

## Bank transfer (virtual account)

`POST /checkout/{invoiceId}/va`

Creates (or returns the existing active) virtual account for the invoice.

```json theme={null}
// body — optional, defaults to 15 minutes
{ "expiryMinutes": 15 }
```

`expiryMinutes` accepts `10`, `15` or `30`; anything else defaults to `15`.

```json theme={null}
// 201 Created
{
  "success": true,
  "message": "Virtual account generated — transfer the exact amount before it expires",
  "data": {
    "accountNumber": "1234567890",
    "accountName": "HYPARROW/Website redesign",
    "bankName": "Wema Bank",
    "bankCode": "035",
    "expiresAt": "2026-06-30T20:15:00Z"
  }
}
```

If a non-expired VA already exists it is returned with `200` instead of creating
a duplicate.

`GET /checkout/{invoiceId}/va` returns the current active, non-expired account,
or `404` if there is none (e.g. it expired).

<Warning>
  The customer must transfer the **exact** amount before `expiresAt`. After
  expiry the account stops accepting payment — call `POST …/va` again to mint a
  fresh one. Bank transfer is **NGN only**; a non-NGN invoice returns `400`.
</Warning>

## USSD

`POST /checkout/{invoiceId}/ussd`

Generates a USSD dial code for the customer's bank.

```json theme={null}
// body
{ "bankCode": "035" }
```

```json theme={null}
{
  "success": true,
  "message": "USSD code generated. Dial the code on your phone to complete payment.",
  "merchantTransactionReference": "chk_ussd_3f9a…",
  "data": {
    "ussdCode": "*945*000*1234#",
    "reference": "…",
    "responseCode": "…",
    "merchantTransactionReference": "chk_ussd_3f9a…"
  }
}
```

The customer dials the code to authorize the transfer. USSD is **NGN only**.
A `checkout.payment.pending` callback fires when the code is generated.

## Card

`POST /checkout/{invoiceId}/card`

Charges a card via the card network. Card details are encrypted before they
leave the browser.

```json theme={null}
// body
{
  "pan": "5060990580000217499",
  "pin": "1234",
  "expiryDate": "2512",
  "cvv2": "123",
  "amount": "377037500",
  "currency": "NGN"
}
```

The response carries the network's `responseCode`:

* `00` — approved. The invoice is settled and a `checkout.payment.completed`
  callback fires.
* `T0` / `S0` — processing (e.g. an OTP/3-DS step). A `checkout.payment.pending`
  callback fires; keep polling status.
* anything else — declined. A `checkout.payment.failed` callback fires.

```json theme={null}
{ "success": true, "message": "Card purchase initiated", "data": { "responseCode": "00", "message": "Approved" } }
```

## OPay

`POST /checkout/{invoiceId}/opay`

Initializes an OPay payment and returns a `redirectUrl` to send the customer to.

```json theme={null}
{
  "success": true,
  "message": "OPay payment initialized. Redirect customer to the provided URL.",
  "redirectUrl": "https://…",
  "transactionReference": "chk_opay_…",
  "responseCode": "00"
}
```

After authorizing on OPay the customer is returned to the checkout page, which
polls status to confirm. A `checkout.payment.pending` callback fires on
initialization.

## Crypto

`POST /checkout/{invoiceId}/crypto`

Generates a stablecoin payment address. The fiat total is converted to USD.

```json theme={null}
// body — optional, defaults to USDC on Base
{ "chain": "BASE", "token": "USDC" }
```

```json theme={null}
// 201 Created
{
  "success": true,
  "data": {
    "id": "…",
    "address": "0xabc…",
    "network": "BASE",
    "asset": "USDC",
    "amountInAsset": "242.000000",
    "amountFiat": 377037500,
    "currencyFiat": "NGN",
    "amountUsd": 242.0,
    "status": "pending",
    "expiresAt": "2026-06-30T20:30:00Z",
    "isActive": true
  }
}
```

`token` is `USDC` or `USDT`. The customer sends the asset to `address` before
`expiresAt` (30 minutes). `GET /checkout/{invoiceId}/crypto` returns the active
address. Settlement arrives asynchronously once the on-chain transfer confirms.

## Poll status

`GET /checkout/{invoiceId}/status`

The checkout page polls this until the payment lands.

```json theme={null}
{ "success": true, "data": { "status": "paid", "paid": true } }
```

`paid` is `true` once the invoice is `paid` (or a receipt exists). Use this as
the source of truth on the page; the asynchronous methods (transfer, USSD, OPay,
crypto) flip it when settlement confirms.

## Payment links

A **payment link** lets you collect a payment without pre-creating an invoice —
useful for a shareable "pay me" page. The public URL is
`CHECKOUT_BASE_URL/pay/{identifier}`, where `identifier` is the link's custom
slug or its id.

`GET /checkout/pay/{identifier}` returns the link's page data (name,
description, amount, allowed methods):

```json theme={null}
{
  "success": true,
  "data": {
    "id": "…",
    "slug": "donate",
    "type": "one_time",
    "pageName": "Support our work",
    "amount": 500000,
    "currency": "NGN",
    "allowedPaymentMethods": ["bankTransfer", "card", "opay", "crypto"]
  }
}
```

`POST /checkout/pay/{identifier}` turns the customer's submission into an invoice
so the normal payment rails can settle it. If the link has a fixed `amount` it
wins; otherwise the customer-entered `amount` is used.

```json theme={null}
// body
{ "amount": 500000, "customerName": "Ada Obi", "customerEmail": "ada@example.com" }
```

```json theme={null}
// 201 Created
{
  "success": true,
  "message": "Payment initialized",
  "data": { "invoiceId": "9a0b…" }
}
```

Hand the returned `invoiceId` to the standard checkout flow above
(`…/va`, `…/card`, etc.) to complete payment.

## Edge cases

| Situation                   | Behaviour                                                                              |
| --------------------------- | -------------------------------------------------------------------------------------- |
| Invoice already `paid`      | The payment-start endpoints return `400 "invoice is already paid"`.                    |
| Invoice `canceled`          | `GET /checkout/{invoiceId}` and the start endpoints return `404 "invoice not found"`.  |
| Expired virtual account     | `GET …/va` returns `404`; mint a new one with `POST …/va`.                             |
| Amount mismatch on transfer | Only a transfer matching the invoice amount settles it. Pay the exact `payableAmount`. |
| Non-NGN invoice             | `bankTransfer` and `ussd` are hidden / rejected; use card or crypto.                   |

## Checkout callback webhook

If the invoice has a `checkoutCallbackUrl`, Hyparrow `POST`s an event to it as
the payment progresses. The body is:

```json theme={null}
{
  "event": "checkout.payment.completed",
  "timestamp": 1751313600,
  "data": {
    "type": "invoice",
    "invoiceId": "8f1c2e34-5a6b-47c8-9d0e-112233445566",
    "status": "paid",
    "amount": 376250000,
    "currency": "NGN",
    "reference": "chk_card_…",
    "paymentMethod": "card"
  }
}
```

Events:

* `checkout.payment.pending` — payment started, awaiting settlement (transfer
  pending, USSD/OPay in progress, card in 3-DS).
* `checkout.payment.completed` — settled; the invoice is `paid`.
* `checkout.payment.failed` — the attempt failed (e.g. a declined card).

Treat the callback as a notification and confirm with
[`GET /checkout/{invoiceId}/status`](#poll-status) before fulfilling.

## Sandbox

In the [sandbox](/sandbox), the hosted checkout URL carries `?sandbox=true` and
points at the sandbox API (`https://sandbox.hyparrow.cloud/api/v1`). You don't
need a real bank transfer or card to test — complete the payment instantly:

<Note>
  Use the **"Simulate payment"** button on the sandbox checkout page, or call
  `POST /checkout/{invoiceId}/simulate-payment` directly. It runs the invoice
  through the normal payment path — marking it `paid`, issuing a receipt and
  firing the `checkout.payment.completed` callback — with simulated money. It is
  available **only** on the sandbox deployment (returns `404` in production).
</Note>

```bash theme={null}
curl -X POST https://sandbox.hyparrow.cloud/api/v1/checkout/{invoiceId}/simulate-payment
```

```json theme={null}
{ "success": true, "message": "Sandbox payment completed", "data": { "status": "paid" } }
```

If the invoice is already paid, it returns `200` with `"invoice already paid"`.
