# Checkouts

Source: https://caravelpay.app/docs/checkouts.html · Caravel Pay docs · Last updated 2026-11-09

A checkout is a hosted payment page for one order. Create it on your server, send the buyer to its URL, and read it back whenever you need its current state. List them to reconcile your orders.

## The checkout object

| Field | Description |
|---|---|
| `id` · string | Unique checkout id, for example `chk_8Qe2TnV4kR`. |
| `checkout_url` · string | The hosted checkout. Redirect the buyer here. |
| `status` · string | One of `awaiting_payment`, `confirming`, `paid`, `underpaid`, `expired`, `review`. See [statuses](https://caravelpay.app/docs/statuses.html). |
| `amount` · string | The amount you asked for, with two decimals, for example `"136.41"`. |
| `currency` · string | Always `"USD"` in v1. |
| `expires_at` · string | When the checkout stops accepting payment (ISO 8601, UTC). 60 minutes after creation. |
| `order_id` · string | Your order reference, as you sent it. Returned by `GET`, the list and webhooks. |
| `created_at` · string | When the checkout was created (ISO 8601, UTC). Returned by `GET`, the list and webhooks. |
| `paid_amount` · string | How much has arrived so far, in USD. Returned by `GET`, the list and webhooks. |
| `payout` · object | Present once the checkout is `paid`: `amount` (string), `asset` (`"USDC"`, `"USDC.e"` or `"POL"`: the token the buyer paid with, never swapped) and `tx_hash` of the transfer to your wallet. |
| `payouts` · array | Only when a checkout was paid in more than one token (for example USDC and USDC.e): every transfer to your wallet, oldest first, each shaped like `payout`. `payout` is the first of them. Add the amounts up for the total. Omitted when there is a single transfer. |

## Create a checkout

`POST/v1/checkouts`

"Your site" below means the `site_url` you sent when you [got your keys](https://caravelpay.app/docs/connect.html#site-url), with or without `www`.

| Parameter | Description |
|---|---|
| `order_id` · string · required | Your order reference. Unique per merchant: sending the same `order_id` again returns the same checkout. |
| `amount` · string · required | Order total as a string with exactly two decimals, for example `"136.41"`. |
| `currency` · string · required | `"USD"`. |
| `customer_email` · string · optional | Pre-fills the buyer's email on the hosted checkout. |
| `return_url` · string · required | Where the buyer goes after paying. HTTPS on your site. Not proof of payment. |
| `cancel_url` · string · required | Where the buyer goes if they cancel. HTTPS on your site. |
| `callback_url` · string · required | HTTPS endpoint on your site that receives [webhooks](https://caravelpay.app/docs/webhooks.html) for this checkout. |

*POST /v1/checkouts · curl*

```sh
BODY='{"order_id":"1042","amount":"136.41","currency":"USD","customer_email":"buyer@example.com","return_url":"https://shop.example/thanks","cancel_url":"https://shop.example/cart","callback_url":"https://shop.example/webhooks/caravel"}'
TS=$(date +%s)
SIG=$(printf '%s\n%s\n%s\n%s' "$TS" POST /v1/checkouts "$BODY" \
  | openssl dgst -sha256 -hmac "$CARAVEL_SECRET" | awk '{print $NF}')

curl https://api.caravelpay.app/v1/checkouts \
  -H "Content-Type: application/json" \
  -H "Caravel-Merchant: $CARAVEL_MERCHANT" \
  -H "Caravel-Timestamp: $TS" \
  -H "Caravel-Signature: $SIG" \
  --data-binary "$BODY"
```

*POST /v1/checkouts · Node.js*

```js
import { caravel } from "./caravel.mjs"; // signing helper from the Quickstart

const checkout = await caravel("POST", "/v1/checkouts", {
  order_id: "1042",
  amount: "136.41",
  currency: "USD",
  customer_email: "buyer@example.com",
  return_url: "https://shop.example/thanks",
  cancel_url: "https://shop.example/cart",
  callback_url: "https://shop.example/webhooks/caravel",
});

res.redirect(303, checkout.checkout_url);
```

*POST /v1/checkouts · PHP*

```php
<?php
require __DIR__ . '/caravel.php'; // signing helper from the Quickstart

$checkout = caravel('POST', '/v1/checkouts', [
    'order_id'       => '1042',
    'amount'         => number_format($order_total, 2, '.', ''),
    'currency'       => 'USD',
    'customer_email' => 'buyer@example.com',
    'return_url'     => 'https://shop.example/thanks',
    'cancel_url'     => 'https://shop.example/cart',
    'callback_url'   => 'https://shop.example/webhooks/caravel',
]);

header('Location: ' . $checkout['checkout_url'], true, 303);
```

*POST /v1/checkouts · Python*

```py
from caravel import caravel  # signing helper from the Quickstart

checkout = caravel("POST", "/v1/checkouts", {
    "order_id": "1042",
    "amount": f"{order_total:.2f}",
    "currency": "USD",
    "customer_email": "buyer@example.com",
    "return_url": "https://shop.example/thanks",
    "cancel_url": "https://shop.example/cart",
    "callback_url": "https://shop.example/webhooks/caravel",
})

return redirect(checkout["checkout_url"], code=303)
```

*201 Created*

```json
{
  "id": "chk_8Qe2TnV4kR",
  "checkout_url": "https://pay.caravelpay.app/c/chk_8Qe2TnV4kR",
  "status": "awaiting_payment",
  "amount": "136.41",
  "currency": "USD",
  "expires_at": "2026-11-09T15:04:12Z"
}
```

### Retries and order_id

Network calls fail. Because `order_id` is unique per merchant, a retry with the same `order_id` returns the checkout you already created instead of making a second one. Retry timeouts and `5xx` responses freely.

A checkout expires 60 minutes after it is created. If the buyer wants to try again after that, create a new checkout with a new `order_id`, for example `1042-2`.

## Retrieve a checkout

`GET/v1/checkouts/{id}`

Returns the checkout with its current `status`, `paid_amount` and, once paid, the `payout`. Sign it like any request, with an empty body.

*GET /v1/checkouts/{id} · curl*

```sh
TS=$(date +%s)
PATH_=/v1/checkouts/chk_8Qe2TnV4kR
SIG=$(printf '%s\n%s\n%s\n%s' "$TS" GET "$PATH_" "" \
  | openssl dgst -sha256 -hmac "$CARAVEL_SECRET" | awk '{print $NF}')

curl "https://api.caravelpay.app$PATH_" \
  -H "Caravel-Merchant: $CARAVEL_MERCHANT" \
  -H "Caravel-Timestamp: $TS" \
  -H "Caravel-Signature: $SIG"
```

*GET /v1/checkouts/{id} · Node.js*

```js
const checkout = await caravel("GET", `/v1/checkouts/${checkoutId}`);
```

*GET /v1/checkouts/{id} · PHP*

```php
$checkout = caravel('GET', '/v1/checkouts/' . rawurlencode($checkoutId));
```

*GET /v1/checkouts/{id} · Python*

```py
checkout = caravel("GET", f"/v1/checkouts/{checkout_id}")
```

*200 OK*

```json
{
  "id": "chk_8Qe2TnV4kR",
  "checkout_url": "https://pay.caravelpay.app/c/chk_8Qe2TnV4kR",
  "status": "paid",
  "amount": "136.41",
  "currency": "USD",
  "expires_at": "2026-11-09T15:04:12Z",
  "order_id": "1042",
  "created_at": "2026-11-09T14:04:12Z",
  "paid_amount": "136.41",
  "payout": {
    "amount": "133.68",
    "asset": "USDC",
    "tx_hash": "0x5d1e8c0b7a4f2e19c6d3b8a0f5e7c2d1a9b4e6f8c0d2a7b5e3f1c9d8a6b4a07b"
  }
}
```

## List checkouts

`GET/v1/checkouts`

Your checkouts, newest first, as full checkout objects. Use it to reconcile orders, or to find a payout's transaction hash.

| Query parameter | Description |
|---|---|
| `limit` · integer · optional | Page size, from 1 to 100. Default 20. |
| `cursor` · string · optional | The `next_cursor` from the previous page. |

Sign the path with its query string, exactly as you send it: `/v1/checkouts?limit=50`, then `/v1/checkouts?limit=50&cursor=…`. Pass the cursor through as you got it; don't build one yourself.

*GET /v1/checkouts · curl*

```sh
TS=$(date +%s)
PATH_='/v1/checkouts?limit=50'
SIG=$(printf '%s\n%s\n%s\n%s' "$TS" GET "$PATH_" "" \
  | openssl dgst -sha256 -hmac "$CARAVEL_SECRET" | awk '{print $NF}')

curl "https://api.caravelpay.app$PATH_" \
  -H "Caravel-Merchant: $CARAVEL_MERCHANT" \
  -H "Caravel-Timestamp: $TS" \
  -H "Caravel-Signature: $SIG"
```

*GET /v1/checkouts · Node.js*

```js
// Walk every page, newest first
let cursor = null;
do {
  const query = new URLSearchParams({ limit: "100", ...(cursor ? { cursor } : {}) });
  const page = await caravel("GET", `/v1/checkouts?${query}`);
  for (const checkout of page.data) reconcile(checkout);
  cursor = page.next_cursor;
} while (cursor);
```

*GET /v1/checkouts · PHP*

```php
// Walk every page, newest first
$cursor = null;
do {
    $query = http_build_query(array_filter(['limit' => 100, 'cursor' => $cursor]));
    $page = caravel('GET', '/v1/checkouts?' . $query);
    foreach ($page['data'] as $checkout) {
        reconcile($checkout);
    }
    $cursor = $page['next_cursor'];
} while ($cursor !== null);
```

*GET /v1/checkouts · Python*

```py
# Walk every page, newest first
from urllib.parse import urlencode

cursor = None
while True:
    query = urlencode({"limit": 100, **({"cursor": cursor} if cursor else {})})
    page = caravel("GET", f"/v1/checkouts?{query}")
    for checkout in page["data"]:
        reconcile(checkout)
    cursor = page["next_cursor"]
    if cursor is None:
        break
```

*200 OK*

```json
{
  "data": [
    {
      "id": "chk_8Qe2TnV4kR",
      "checkout_url": "https://pay.caravelpay.app/c/chk_8Qe2TnV4kR",
      "status": "paid",
      "amount": "136.41",
      "currency": "USD",
      "expires_at": "2026-11-09T15:04:12Z",
      "order_id": "1042",
      "created_at": "2026-11-09T14:04:12Z",
      "paid_amount": "136.41",
      "payout": {
        "amount": "133.68",
        "asset": "USDC",
        "tx_hash": "0x5d1e8c0b7a4f2e19c6d3b8a0f5e7c2d1a9b4e6f8c0d2a7b5e3f1c9d8a6b4a07b"
      }
    }
  ],
  "next_cursor": null
}
```

`next_cursor` is `null` on the last page.

## Return and cancel URLs

After paying, the buyer is sent to `return_url`; if they back out, to `cancel_url`. Use these for the buyer's experience only. Payment can still be confirming when they land, and anyone can open those URLs, so fulfil orders from the [webhook](https://caravelpay.app/docs/webhooks.html) or a `GET` that shows `paid`.
