@bitpal/mcp 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/stdio.js +794 -50
- package/package.json +1 -1
package/dist/stdio.js
CHANGED
|
@@ -4067,7 +4067,7 @@ var docs_data_default = {
|
|
|
4067
4067
|
title: "API reference",
|
|
4068
4068
|
url: "https://bitpal.io/docs/#api",
|
|
4069
4069
|
markdownUrl: "https://bitpal.io/docs/md/api.md",
|
|
4070
|
-
markdown: '# API reference\n\nEvery public endpoint, generated from the OpenAPI 3.1.0 spec (version 1.0.0). Download it: [openapi.json](/docs/openapi.json).\n\n## Basics\n\n| Topic | Details |\n| --- | --- |\n| Base URL | `https://api.bitpal.io` \u2014 test and live share the host; the API key prefix selects the mode. |\n| Auth | `Authorization: Bearer bp_test_\u2026` (test) or `bp_live_\u2026` (live). Keep keys on your server. |\n| Format | JSON. Success: `{ "data": \u2026 }` (authenticated endpoints add `meta: { mode, requestId }`). Amounts are strings. |\n| Errors | `{ "error", "code", "retryable", "trace_id", "details"? }` \u2014 `error` is the human-readable message; branch on `code`. See [Errors](/docs/md/errors.md). |\n| Idempotency | `Idempotency-Key` (8\u2013128 chars) is honoured on every authenticated merchant `POST`; endpoints that do not enforce it say so. |\n\n## Checkout sessions\n\nCreate a session per order, send the buyer to the hosted page (or build your own UI), and track it.\n\n### `POST /v1/checkout/sessions` \u2014 Create a checkout session\n\nCreates a session for one order and returns the hosted `checkout_url`. Line items take either `amount` (plain decimal string, e.g. "29") or `amount_atomic` (6-decimal integer string), or reference a `price_id`. **Minimum amount:** the session total (sum of `amount \xD7 quantity`) must be at least 1.00 in the session currency (USDC/USDT: $1, JPYC: \xA51); a lower total returns 400 `CHECKOUT_AMOUNT_TOO_LOW`. Some networks also have a higher per-network minimum (Tron: currently $300, testnet included; JPYC: \xA5100) \u2014 a network whose minimum is above the total is not offered to the buyer, and requesting a deposit address on it returns 400 `CHECKOUT_BELOW_MIN_PAYMENT`. **Payout wallet:** you need a payout wallet registered in the session\'s mode (test/live). With `pay_chain`, a wallet for that network\'s type (EVM, Tron or Solana) is required. With `allowed_pay_chains` / `allowed_assets`, at least one listed network must have a wallet of its type, and the buyer is only offered networks you can receive on. Without any network restriction, any payout wallet is enough: with an EVM wallet the buyer is offered Base USDC as before; without one, the networks of the wallet types you registered (if the total is below the minimum of all of them, 400 `CHECKOUT_BELOW_MIN_PAYMENT`). No usable payout wallet returns 400 `SELLER_WALLET_NOT_CONFIGURED`.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`CreateCheckoutSessionRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `line_items` | array of (object \\| any \\| any) | Yes | Raw items (`name`, `currency`, `amount` XOR `amount_atomic`) or price items (`price_id`). Do not mix the two kinds. The total (sum of `amount \xD7 quantity`) must be at least 1.00 in the session currency, otherwise 400 `CHECKOUT_AMOUNT_TOO_LOW`. (\u2265 1 items) |\n| `currency` | string | No | Session currency. Defaults to the line items\u2019 currency. (\u2265 1 chars) |\n| `success_url` | string (uri) | No | Where the buyer returns after paying. `{CHECKOUT_SESSION_ID}` is replaced with the session id. (\u2264 2048 chars) |\n| `cancel_url` | string (uri) | No | Where the buyer goes when cancelling. (\u2264 2048 chars) |\n| `customer_email` | string (email) | No | Buyer email. Pre-fills the receipt form on the payment completion screen; no receipt is sent automatically \u2014 the buyer requests it there. |\n| `metadata` | map | No | Your key/value data, echoed back on the session and in webhooks (`data.metadata`). Reserved keys BitPal writes itself (`_bitpal`, `payer_email`, `allowed_pay_chains`, `allowed_assets`) are not returned \u2014 do not use them. |\n| `expires_in_minutes` | integer | No | Payment window in minutes (1\u201360). Default 15. (1\u201360, default `15`) |\n| `tolerance_percent` | string | No | Deprecated. Underpayments are never accepted \u2014 a short payment becomes `unresolved_underpaid` and can be topped up to the same address. Only `"0"` is accepted; any other value returns 400. |\n| `pay_chain` | string | No | Fix the payment network: CAIP-2 (`eip155:84532`) or a short name (`base`), which resolves to the testnet in test mode and the mainnet in live mode. Mutually exclusive with `allowed_pay_chains`. |\n| `allowed_pay_chains` | array of string | No | Networks the buyer may choose from (CAIP-2 or short names, resolved like `pay_chain`). (1\u20138 items) |\n| `allowed_assets` | array of object | No | Allowed (chain, token) cells. Takes precedence over `allowed_pay_chains`. (1\u201316 items) |\n| `late_payments` | "accept" \\| "refund" | No | What to do with a full payment that arrives after the payment deadline. `accept` (default): accepted as a late payment for up to 7 days (`checkout.session.paid` with `late: true`). `refund`: never accepted \u2014 it is refunded automatically to the buyer about 7 days after the deadline (refund kind `late_deposit`); the non-custodial deposit contract does not allow an earlier refund. |\n\n```\n{\n "line_items": [\n {\n "name": "Pro plan",\n "amount": "29",\n "currency": "USDC"\n }\n ],\n "pay_chain": "eip155:84532",\n "expires_in_minutes": 30,\n "success_url": "https://your.store/thanks?session={CHECKOUT_SESSION_ID}"\n}\n```\n\n**Response `201`** \u2014 Session created.\n\n```\n{\n "data": {\n "id": "cs_Q2FxKhomCEju_OqvKfrQsTOh",\n "checkout_url": "https://bitpal.io/pay/cs_Q2FxKhomCEju_OqvKfrQsTOh?token=<session_token>",\n "session_token": "<session_token>",\n "status": "created",\n "amount_total": "29000000",\n "currency": "USDC",\n "expires_at": "2026-11-06T07:58:28.776Z",\n "tax": {\n "tax_amount": "0",\n "total_with_tax": "29000000"\n }\n },\n "meta": {\n "mode": "TEST",\n "requestId": "ec091faf-\u2026"\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `409` \xB7 `422` \xB7 `429` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/sessions` \u2014 List checkout sessions\n\nNewest first, in the API key\u2019s mode (test/live).\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `status` | query | string | No | Filter by status, or `succeeded` for all successful states. |\n| `limit` | query | integer | No | 1\u2013100, default `50` |\n| `offset` | query | integer | No | \u2265 0, default `0` |\n\n**Response `200`** \u2014 Sessions.\n\n```\n{\n "data": [\n {\n "id": "string",\n "status": "string",\n "mode": "TEST",\n "amount_total": "1000000",\n "amount_received": "string",\n "currency": "string",\n "pay_chain": "string",\n "pay_asset": "string",\n "token_decimals": 0,\n "payment_method": "string",\n "payer_wallet": "string",\n "seller_wallet": "string",\n "tolerance_percent": "string",\n "late_payments": "accept",\n "deposit_address": "string",\n "refund_address": "string",\n "received_token": "string",\n "received_token_symbol": "string",\n "fee_amount": "string",\n "fee_rate": "string",\n "net_amount": "string",\n "fee_amount_token": "string",\n "net_amount_token": "string",\n "tx_hash": "string",\n "deposit_tx_hash": "string",\n "sweep_tx_hash": "string",\n "success_url": "string",\n "cancel_url": "string",\n "metadata": {},\n "payment_source": "string",\n "payment_link_id": "string",\n "expires_at": "2026-11-06T07:00:00.000Z",\n "paid_at": "2026-11-06T07:00:00.000Z",\n "settled_at": "2026-11-06T07:00:00.000Z",\n "created_at": "2026-11-06T07:00:00.000Z",\n "raw_status": "string"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/sessions/{id}` \u2014 Retrieve a checkout session\n\nServer-side source of truth for an order, together with webhooks.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Checkout session id (`cs_\u2026`). |\n\n**Response `200`** \u2014 The session.\n\n```\n{\n "data": {\n "id": "string",\n "status": "string",\n "mode": "TEST",\n "amount_total": "1000000",\n "amount_received": "string",\n "currency": "string",\n "pay_chain": "string",\n "pay_asset": "string",\n "token_decimals": 0,\n "payment_method": "string",\n "payer_wallet": "string",\n "seller_wallet": "string",\n "tolerance_percent": "string",\n "late_payments": "accept",\n "deposit_address": "string",\n "refund_address": "string",\n "received_token": "string",\n "received_token_symbol": "string",\n "fee_amount": "string",\n "fee_rate": "string",\n "net_amount": "string",\n "fee_amount_token": "string",\n "net_amount_token": "string",\n "tx_hash": "string",\n "deposit_tx_hash": "string",\n "sweep_tx_hash": "string",\n "success_url": "string",\n "cancel_url": "string",\n "metadata": {},\n "payment_source": "string",\n "payment_link_id": "string",\n "expires_at": "2026-11-06T07:00:00.000Z",\n "paid_at": "2026-11-06T07:00:00.000Z",\n "settled_at": "2026-11-06T07:00:00.000Z",\n "created_at": "2026-11-06T07:00:00.000Z",\n "line_items": [\n {\n "id": "string",\n "name": "string",\n "amount": "1000000",\n "currency": "string",\n "quantity": 0,\n "price_id": "string",\n "product_id": "string",\n "created_at": "2026-11-06T07:00:00.000Z"\n }\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/sessions/{id}/status` \u2014 Get session status\n\nLightweight status for polling from your server. Requires your API key (N13 \u2014 it is not a public endpoint). Buyer-facing pages should use `GET /v1/checkout/sessions/{id}/public`.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Checkout session id (`cs_\u2026`). |\n\n**Response `200`** \u2014 Status.\n\n```\n{\n "data": {\n "status": "awaiting_deposit",\n "paid_at": null\n }\n}\n```\n\n**Errors:** `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/sessions/{id}/public` \u2014 Get the public session view\n\nBuyer-facing view used by the checkout page: payment options, deposit address once issued, refunds. No authentication; CORS-open.\n\n**Auth:** None \u2014 public endpoint\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Checkout session id (`cs_\u2026`). |\n\n**Response `200`** \u2014 Public view.\n\n```\n{\n "data": {\n "id": "string",\n "status": "string",\n "token_decimals": 0,\n "pay_amount": "string",\n "mode": "TEST",\n "chain_id": 0,\n "token_address": "string",\n "payment_options": [\n {\n "caip2": "string",\n "chain_id": 0,\n "token_symbol": "string",\n "token_address": "string",\n "decimals": 0,\n "amount_atomic": "string",\n "available": true,\n "unavailable_reason": "coming_soon"\n }\n ],\n "currency": "string",\n "amount_total": "1000000",\n "amount_received": "string",\n "chain": "string",\n "tx_hash": "string",\n "release_tx_hash": "string",\n "expires_at": "2026-11-06T07:00:00.000Z",\n "success_url": "string",\n "cancel_url": "string",\n "fee_amount": "string",\n "fee_rate": "string",\n "net_amount": "string",\n "tax_amount": null,\n "tax_rate": null,\n "pay_chain": "string",\n "pay_asset": "string",\n "deposit_address": "string",\n "deposit_owner": "string",\n "payment_deadline_at": "2026-11-06T07:00:00.000Z",\n "payment_method": "string",\n "payer_wallet": "string",\n "seller_wallet": "string",\n "tolerance_percent": "string",\n "paid_at": "2026-11-06T07:00:00.000Z",\n "settled_at": "2026-11-06T07:00:00.000Z",\n "created_at": "2026-11-06T07:00:00.000Z",\n "merchant_name": "string",\n "line_items": [\n {\n "name": "string",\n "amount": "1000000",\n "currency": "string",\n "quantity": 0\n }\n ],\n "payment_source": "string",\n "refunds": [\n {\n "kind": "string",\n "amount": "string",\n "token_symbol": "string",\n "decimals": 0,\n "tx_hash": "string",\n "caip2": "string"\n }\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `POST /v1/checkout/sessions/{id}/deposit-address` \u2014 Issue the deposit address\n\nFixes the buyer\u2019s network, token and refund address, and returns the one-time deposit address with the exact amount. Calling again returns the same address and keeps the first refund address. Requires the session token (header `X-Session-Token` or body `session_token`).\n\n**Auth:** Session token \u2014 `X-Session-Token` header or `session_token` body field (no API key)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Checkout session id (`cs_\u2026`). |\n| `Idempotency-Key` | header | string | No | Accepted but not yet enforced on this endpoint \u2014 retries are not de-duplicated by key. (8\u2013128 chars) |\n| `X-Session-Token` | header | string | No | Session token from the create-session response. Alternative to the `session_token` body field. |\n\n**Request body** (`IssueDepositAddressRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `session_token` | string | No | From the create-session response. May be sent as the `X-Session-Token` header instead. |\n| `refundAddress` | string | Yes | Buyer refund address in the chosen network\u2019s format (EVM 0x\u2026, Tron T\u2026, Solana base58). Cannot change later. |\n| `chain` | string | No | CAIP-2 or short name of the chosen network. Required for multi-option sessions. (\u2265 1 chars) |\n| `token` | string | No | Token symbol of the chosen asset, e.g. USDC. Required for multi-option sessions. (\u2265 1 chars) |\n\n```\n{\n "session_token": "<session_token>",\n "refundAddress": "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720",\n "chain": "eip155:84532",\n "token": "USDC"\n}\n```\n\n**Response `200`** \u2014 Deposit address.\n\n```\n{\n "data": {\n "sessionId": "cs_\u2026",\n "chain": "eip155:84532",\n "depositAddress": "0x6b19e871c934aB52775B6712e110f3e4b0626347",\n "expectedToken": "0x943b94dacf1507f2658f1cfe103ac36ebc13ef65",\n "expectedAmount": "29000000",\n "feeAmount": "435000",\n "refundAddress": "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720",\n "deadline": "1793951938",\n "expiresAt": "2026-11-06T07:58:28.776Z",\n "depositOwner": null\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `404` \xB7 `409` \xB7 `410` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/sessions/{id}/refunds` \u2014 List refunds for a session\n\nRefunds recorded for this session: automatic on-chain refunds from the deposit address (overpayment, expired underpayment, wrong token, late deposit) and refunds you sent from your own wallet and recorded in the console (`kind: merchant`). There is no API to make a refund \u2014 BitPal is non-custodial; refund a completed payment from Console \u2192 Sessions \u2192 session \u2192 Refund.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Checkout session id (`cs_\u2026`). |\n\n**Response `200`** \u2014 Refunds.\n\n```\n{\n "data": [\n {\n "id": "string",\n "kind": "string",\n "token": "string",\n "token_symbol": "string",\n "amount": "string",\n "amount_display": "string",\n "decimals": 0,\n "refund_address": "string",\n "tx_hash": "string",\n "caip2": "string",\n "block_number": "string",\n "created_at": "2026-11-06T07:00:00.000Z",\n "from_address": "string",\n "reason_code": "string",\n "note": "string",\n "explorer_url": "string"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/fee-preview` \u2014 Preview the fee for an amount\n\nPass exactly one of `amount` or `amount_atomic`. The response is always atomic.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `amount` | query | string | No | Decimal string, e.g. "100". |\n| `amount_atomic` | query | string | No | 6-decimal integer string, e.g. "100000000". |\n\n**Response `200`** \u2014 Fee breakdown.\n\n```\n{\n "data": {\n "amount": "100000000",\n "fee_rate": "0.015",\n "fee_amount": "1500000",\n "net_amount": "98500000"\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n## Products & prices\n\nCatalog objects. Prices are immutable.\n\n### `POST /v1/checkout/products` \u2014 Create a product\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`CreateProductRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `name` | string | Yes | 1\u2013200 chars |\n| `description` | string | No | \u2264 2000 chars |\n| `image_url` | string (uri) | No | \u2264 2048 chars |\n| `metadata` | map | No | |\n\n```\n{\n "name": "Pro plan"\n}\n```\n\n**Response `201`** \u2014 Product created.\n\n```\n{\n "data": {\n "id": "string",\n "name": "string",\n "description": "string",\n "image_url": "string",\n "status": "active",\n "metadata": {},\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/products` \u2014 List products\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `status` | query | "active" \\| "inactive" | No | |\n| `limit` | query | integer | No | 1\u2013100, default `50` |\n| `offset` | query | integer | No | \u2265 0, default `0` |\n\n**Response `200`** \u2014 Products.\n\n```\n{\n "data": [\n {\n "id": "string",\n "name": "string",\n "description": "string",\n "image_url": "string",\n "status": "active",\n "metadata": {},\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/products/{id}` \u2014 Retrieve a product\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Product id (`prod_\u2026`). |\n\n**Response `200`** \u2014 The product.\n\n```\n{\n "data": {\n "id": "string",\n "name": "string",\n "description": "string",\n "image_url": "string",\n "status": "active",\n "metadata": {},\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `PATCH /v1/checkout/products/{id}` \u2014 Update a product\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Product id (`prod_\u2026`). |\n\n**Request body** (`UpdateProductRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `name` | string | No | 1\u2013200 chars |\n| `description` | string \\| null | No | \u2264 2000 chars |\n| `image_url` | string (uri) \\| null | No | \u2264 2048 chars |\n| `metadata` | map | No | |\n| `status` | "active" \\| "inactive" | No | |\n\n**Response `200`** \u2014 The updated product.\n\n```\n{\n "data": {\n "id": "string",\n "name": "string",\n "description": "string",\n "image_url": "string",\n "status": "active",\n "metadata": {},\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `POST /v1/checkout/products/{id}/prices` \u2014 Create a price\n\nPrices are immutable: to change an amount, create a new price. Pass exactly one of `amount` (decimal string) or `amount_atomic` (6-decimal integer string). The currency is derived from `token_symbol`.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Product id (`prod_\u2026`). |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`CreatePriceRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `amount` | string | No | Decimal string, e.g. "29". Mutually exclusive with `amount_atomic`. |\n| `amount_atomic` | string | No | 6-decimal integer string ("29000000" = 29). |\n| `token_symbol` | "USDC" \\| "USDT" \\| "JPYC" | No | The price\u2019s asset. The currency is derived from it. (default `"USDC"`) |\n| `billing_type` | "one_time" | No | Only `one_time` for now. (default `"one_time"`) |\n| `nickname` | string | No | Display name. (\u2264 200 chars) |\n| `lookup_key` | string | No | Your own stable key for this price. (1\u2013200 chars) |\n\n```\n{\n "amount": "29",\n "token_symbol": "USDC"\n}\n```\n\n**Response `201`** \u2014 Price created.\n\n```\n{\n "data": {\n "id": "string",\n "product_id": "string",\n "amount_atomic": "1000000",\n "currency": "string",\n "token_symbol": "string",\n "billing_type": "one_time",\n "nickname": "string",\n "lookup_key": "string",\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/products/{id}/prices` \u2014 List a product\u2019s prices\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Product id (`prod_\u2026`). |\n| `limit` | query | integer | No | 1\u2013100, default `50` |\n| `offset` | query | integer | No | \u2265 0, default `0` |\n\n**Response `200`** \u2014 Prices.\n\n```\n{\n "data": [\n {\n "id": "string",\n "product_id": "string",\n "amount_atomic": "1000000",\n "currency": "string",\n "token_symbol": "string",\n "billing_type": "one_time",\n "nickname": "string",\n "lookup_key": "string",\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/prices/{id}` \u2014 Retrieve a price\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Price id (`price_\u2026`). |\n\n**Response `200`** \u2014 The price.\n\n```\n{\n "data": {\n "id": "string",\n "product_id": "string",\n "amount_atomic": "1000000",\n "currency": "string",\n "token_symbol": "string",\n "billing_type": "one_time",\n "nickname": "string",\n "lookup_key": "string",\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `PATCH /v1/checkout/prices/{id}` \u2014 Update a price\u2019s nickname\n\nOnly `nickname` can change. Sending an amount/currency field returns `PRICE_IMMUTABLE_FIELD`.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Price id (`price_\u2026`). |\n\n**Request body** (`UpdatePriceRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `nickname` | string \\| null | No | \u2264 200 chars |\n\n**Response `200`** \u2014 The updated price.\n\n```\n{\n "data": {\n "id": "string",\n "product_id": "string",\n "amount_atomic": "1000000",\n "currency": "string",\n "token_symbol": "string",\n "billing_type": "one_time",\n "nickname": "string",\n "lookup_key": "string",\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n## Payment links\n\nShareable hosted URLs for a price.\n\n### `POST /v1/checkout/payment-links` \u2014 Create a payment link\n\nA shareable hosted URL for one price. The link \u2194 price binding is fixed after creation.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`CreatePaymentLinkRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `price_id` | string | Yes | The price this link sells (`price_\u2026`). |\n| `title` | string | No | 1\u2013200 chars |\n| `description` | string | No | \u2264 2000 chars |\n| `collect_email` | boolean | No | Ask the buyer for an email. Default true. (default `true`) |\n| `email_required` | boolean | No | Require the email: the hosted link page asks for it and no checkout is created without it. Default true; needs `collect_email`. (default `true`) |\n| `allowed_pay_chains` | array of string | No | Networks the buyer may choose from (CAIP-2 or short names). (1\u20138 items) |\n| `allowed_assets` | array of object | No | Allowed (chain, token) cells. Takes precedence over `allowed_pay_chains`. (1\u201316 items) |\n| `success_url` | string (uri) | No | \u2264 2048 chars |\n| `cancel_url` | string (uri) | No | \u2264 2048 chars |\n| `max_uses` | integer | No | Stop accepting payments after this many paid uses. (\u2265 1) |\n| `expires_at` | string (date-time) | No | ISO-8601 time after which the link stops working. |\n| `metadata` | map | No | |\n\n```\n{\n "price_id": "price_\u2026",\n "allowed_pay_chains": [\n "eip155:8453"\n ]\n}\n```\n\n**Response `201`** \u2014 Link created.\n\n```\n{\n "data": {\n "id": "string",\n "share_token": "string",\n "price_id": "string",\n "title": "string",\n "description": "string",\n "collect_email": true,\n "email_required": true,\n "allowed_pay_chains": [\n "string"\n ],\n "allowed_assets": [\n {\n "caip2": "string",\n "token": "string"\n }\n ],\n "success_url": "string",\n "cancel_url": "string",\n "status": "active",\n "max_uses": 0,\n "paid_use_count": 0,\n "expires_at": "2026-11-06T07:00:00.000Z",\n "metadata": {},\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/payment-links` \u2014 List payment links\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `status` | query | "active" \\| "inactive" | No | |\n| `limit` | query | integer | No | 1\u2013100, default `50` |\n| `offset` | query | integer | No | \u2265 0, default `0` |\n\n**Response `200`** \u2014 Links.\n\n```\n{\n "data": [\n {\n "id": "string",\n "share_token": "string",\n "price_id": "string",\n "title": "string",\n "description": "string",\n "collect_email": true,\n "email_required": true,\n "allowed_pay_chains": [\n "string"\n ],\n "allowed_assets": [\n {\n "caip2": "string",\n "token": "string"\n }\n ],\n "success_url": "string",\n "cancel_url": "string",\n "status": "active",\n "max_uses": 0,\n "paid_use_count": 0,\n "expires_at": "2026-11-06T07:00:00.000Z",\n "metadata": {},\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/payment-links/{id}` \u2014 Retrieve a payment link\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Payment link id (`plink_\u2026`). |\n\n**Response `200`** \u2014 The link, with product/price summary fields.\n\n```\n{\n "data": {\n "id": "string",\n "share_token": "string",\n "price_id": "string",\n "title": "string",\n "description": "string",\n "collect_email": true,\n "email_required": true,\n "allowed_pay_chains": [\n "string"\n ],\n "allowed_assets": [\n {\n "caip2": "string",\n "token": "string"\n }\n ],\n "success_url": "string",\n "cancel_url": "string",\n "status": "active",\n "max_uses": 0,\n "paid_use_count": 0,\n "expires_at": "2026-11-06T07:00:00.000Z",\n "metadata": {},\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `PATCH /v1/checkout/payment-links/{id}` \u2014 Update a payment link\n\nOperational settings and active \u2194 inactive. The price cannot change \u2014 create a new link instead. Deactivating (`status: "inactive"`) accepts an optional `reason_code` (`user_request`, `security_concern`, `rotation`, `no_longer_needed` or `other`, plus an optional `reason_note`); with an API key, a request without it is recorded as `api_request` (the console always asks for a reason). Reactivating accepts the same fields optionally. Every status change is recorded in your audit log with its reason.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Payment link id (`plink_\u2026`). |\n\n**Request body** (`UpdatePaymentLinkRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `title` | string \\| null | No | 1\u2013200 chars |\n| `description` | string \\| null | No | \u2264 2000 chars |\n| `collect_email` | boolean | No | |\n| `email_required` | boolean | No | |\n| `allowed_pay_chains` | array of string \\| null | No | 1\u20138 items |\n| `allowed_assets` | array of object \\| null | No | 1\u201316 items |\n| `success_url` | string (uri) \\| null | No | \u2264 2048 chars |\n| `cancel_url` | string (uri) \\| null | No | \u2264 2048 chars |\n| `status` | "active" \\| "inactive" | No | |\n| `max_uses` | integer \\| null | No | \u2265 1 |\n| `expires_at` | string (date-time) \\| null | No | |\n| `metadata` | map | No | |\n| `reason_code` | "user_request" \\| "security_concern" \\| "rotation" \\| "no_longer_needed" \\| "other" | No | |\n| `reason_note` | string | No | \u2264 500 chars |\n\n**Response `200`** \u2014 The updated link.\n\n```\n{\n "data": {\n "id": "string",\n "share_token": "string",\n "price_id": "string",\n "title": "string",\n "description": "string",\n "collect_email": true,\n "email_required": true,\n "allowed_pay_chains": [\n "string"\n ],\n "allowed_assets": [\n {\n "caip2": "string",\n "token": "string"\n }\n ],\n "success_url": "string",\n "cancel_url": "string",\n "status": "active",\n "max_uses": 0,\n "paid_use_count": 0,\n "expires_at": "2026-11-06T07:00:00.000Z",\n "metadata": {},\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `404` \xB7 `409` \u2014 see [Errors](/docs/md/errors.md).\n\n## x402\n\nCharge per API call with HTTP 402 and EIP-3009 authorizations.\n\n### `GET /v1/x402/assets` \u2014 List x402 assets for this mode\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Response `200`** \u2014 Assets.\n\n```\n{\n "data": {\n "mode": "TEST",\n "assets": [\n {\n "caip2": "string",\n "network": "string",\n "chain_name": "string",\n "symbol": "string",\n "decimals": 0,\n "address": "string"\n }\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \u2014 see [Errors](/docs/md/errors.md).\n\n### `POST /v1/x402/requirements` \u2014 Build the 402 payment requirements\n\nReturns the body your resource server sends with `402 Payment Required`, including the fee split. Pass exactly one of `amount` or `amountAtomic`.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`X402RequirementsRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `network` | string | No | x402 network name (`base`, `base-sepolia`). Defaults to the mode\u2019s first asset. (\u2265 1 chars) |\n| `asset` | string | No | Token symbol. Default USDC. (\u2265 1 chars, default `"USDC"`) |\n| `payTo` | string | Yes | Your payout wallet (0x) \u2014 must be the one registered in Console for this mode, otherwise `PAYTO_NOT_MERCHANT_WALLET`. |\n| `amount` | string | No | Gross amount per call as a decimal string, e.g. "0.1". The fee is taken from it. |\n| `amountAtomic` | string | No | Same as `amount`, as a 6-decimal integer string. |\n| `resource` | string | Yes | The URL being paid for. (1\u20132048 chars) |\n| `description` | string | No | \u2264 512 chars |\n| `mimeType` | string | No | \u2264 128 chars |\n| `maxTimeoutSeconds` | integer | No | How long the payer has to sign (max 3600). Default 300. (\u2264 3600, > 0) |\n\n```\n{\n "payTo": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",\n "amount": "0.1",\n "resource": "https://api.example.com/report"\n}\n```\n\n**Response `200`** \u2014 402 body.\n\n```\n{\n "data": {\n "x402Version": 1,\n "accepts": [\n {\n "scheme": "exact",\n "network": "string",\n "asset": "string",\n "payTo": "string",\n "maxAmountRequired": "1000000",\n "resource": "string",\n "description": "string",\n "mimeType": "string",\n "maxTimeoutSeconds": 0,\n "extra": {\n "name": "string",\n "version": "string",\n "chainId": 0\n },\n "feeBreakdown": {\n "netAmount": "1000000",\n "feeAmount": "1000000",\n "feeBps": 0,\n "feeRecipient": "string"\n }\n }\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \u2014 see [Errors](/docs/md/errors.md).\n\n### `POST /v1/x402/verify` \u2014 Verify an X-PAYMENT header\n\nChecks the EIP-3009 authorization against the requirements. Sends nothing on-chain.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`X402PaymentRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `x402Version` | 1 | Yes | |\n| `paymentPayload` | object | Yes | |\n| `paymentRequirements` | object | Yes | |\n\n**Response `200`** \u2014 Valid.\n\n```\n{\n "data": {\n "valid": true,\n "payment_id": "string",\n "status": "string",\n "idempotent": true\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `409` \u2014 see [Errors](/docs/md/errors.md).\n\n### `POST /v1/x402/settle` \u2014 Settle an x402 payment\n\nBroadcasts the transfer and waits for the receipt. Serve the resource only when `status` is `confirmed`. Settlement is idempotent per authorization \u2014 retry with the same body on 5xx.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`X402PaymentRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `x402Version` | 1 | Yes | |\n| `paymentPayload` | object | Yes | |\n| `paymentRequirements` | object | Yes | |\n\n**Response `200`** \u2014 Settlement result.\n\n```\n{\n "data": {\n "success": true,\n "payment_id": "string",\n "status": "string",\n "tx_hash": "string",\n "fee_tx_hash": "string",\n "network": "string",\n "idempotent": true\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `409` \xB7 `503` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/x402/payments/{id}` \u2014 Retrieve an x402 payment\n\nRecovery path when a settle response was lost \u2014 check here before settling again.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Payment id. |\n\n**Response `200`** \u2014 The payment.\n\n```\n{\n "data": {\n "id": "string",\n "status": "pending",\n "raw_status": "string",\n "resource": "string",\n "chain": "string",\n "asset": "string",\n "decimals": 0,\n "amount": "1000000",\n "gross_amount": "1000000",\n "payer": "string",\n "pay_to": "string",\n "tx_hash": "string",\n "created_at": "2026-11-06T07:00:00.000Z",\n "settled_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/x402/payments` \u2014 List x402 payments\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `limit` | query | integer | No | \u2264 100, > 0, default `20` |\n| `offset` | query | integer | No | \u2265 0, default `0` |\n| `status` | query | string | No | pending \\| confirmed \\| failed \\| needs_review |\n\n**Response `200`** \u2014 Payments.\n\n```\n{\n "data": [\n {\n "id": "string",\n "status": "pending",\n "raw_status": "string",\n "resource": "string",\n "chain": "string",\n "asset": "string",\n "decimals": 0,\n "amount": "1000000",\n "gross_amount": "1000000",\n "payer": "string",\n "pay_to": "string",\n "tx_hash": "string",\n "created_at": "2026-11-06T07:00:00.000Z",\n "settled_at": "2026-11-06T07:00:00.000Z"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n## Reports\n\nReconciliation: payments, payouts and fees for a date range, as JSON pages or one CSV.\n\n### `GET /v1/checkout/reports/payments` \u2014 Payments report\n\nRows: every session **created** in the range, in any status, newest first. Only the API key\u2019s mode (test or live) is included. With `format=json` (default) you get one page (`limit`/`offset`), `meta.total` and a `summary` over the whole range. With `format=csv` you get the whole range as one RFC 4180 CSV file (`text/csv`, CRLF line endings; `limit`/`offset` are ignored) with the same columns in the same order \u2014 empty cells for null, `metadata` as a JSON string. Amounts are atomic integer strings: `amount_total`, `fee_amount` and `net_amount` are 6-decimal; `fee_amount_token` and `net_amount_token` are in the payment token\u2019s decimals (`token_decimals`) and match the on-chain transfers exactly.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `from` | query | string (date) | No | First day to include, UTC date `YYYY-MM-DD`. Omit for no lower bound. |\n| `to` | query | string (date) | No | Last day to include (the whole day), UTC date `YYYY-MM-DD`. Omit for no upper bound. Must not be before `from`. |\n| `format` | query | "json" \\| "csv" | No | `json` (default) or `csv`. (default `"json"`) |\n| `limit` | query | integer | No | JSON page size, 1\u2013100 (default 50). Ignored for CSV. (1\u2013100, default `50`) |\n| `offset` | query | integer | No | JSON page offset (default 0). Ignored for CSV. (\u2265 0, default `0`) |\n\n**Response `200`** \u2014 One page of rows. `meta` carries total/limit/offset; `summary` totals the whole range. With `format=csv` the body is a CSV file instead.\n\n```\n{\n "data": [\n {\n "session_id": "string",\n "created_at": "2026-11-06T07:00:00.000Z",\n "paid_at": "2026-11-06T07:00:00.000Z",\n "settled_at": "2026-11-06T07:00:00.000Z",\n "status": "string",\n "currency": "string",\n "amount_total": "1000000",\n "amount_received": "string",\n "pay_chain": "string",\n "pay_asset": "string",\n "payer_wallet": "string",\n "deposit_tx_hash": "string",\n "sweep_tx_hash": "string",\n "fee_rate": "string",\n "fee_amount": "string",\n "net_amount": "string",\n "token_decimals": 0,\n "fee_amount_token": "string",\n "net_amount_token": "string",\n "metadata": {}\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n },\n "summary": {\n "total_count": 0,\n "total_amount_atomic": "1000000",\n "total_fees_atomic": "1000000",\n "total_net_atomic": "1000000"\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/reports/payouts` \u2014 Payouts report\n\nRows: every session **settled** to your payout wallet in the range (by `settled_at`), newest first. Only the API key\u2019s mode (test or live) is included. With `format=json` (default) you get one page (`limit`/`offset`), `meta.total` and a `summary` over the whole range. With `format=csv` you get the whole range as one RFC 4180 CSV file (`text/csv`, CRLF line endings; `limit`/`offset` are ignored) with the same columns in the same order \u2014 empty cells for null, `metadata` as a JSON string. Amounts are atomic integer strings: `amount_total`, `fee_amount` and `net_amount` are 6-decimal; `fee_amount_token` and `net_amount_token` are in the payment token\u2019s decimals (`token_decimals`) and match the on-chain transfers exactly.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `from` | query | string (date) | No | First day to include, UTC date `YYYY-MM-DD`. Omit for no lower bound. |\n| `to` | query | string (date) | No | Last day to include (the whole day), UTC date `YYYY-MM-DD`. Omit for no upper bound. Must not be before `from`. |\n| `format` | query | "json" \\| "csv" | No | `json` (default) or `csv`. (default `"json"`) |\n| `limit` | query | integer | No | JSON page size, 1\u2013100 (default 50). Ignored for CSV. (1\u2013100, default `50`) |\n| `offset` | query | integer | No | JSON page offset (default 0). Ignored for CSV. (\u2265 0, default `0`) |\n\n**Response `200`** \u2014 One page of rows. `meta` carries total/limit/offset; `summary` totals the whole range. With `format=csv` the body is a CSV file instead.\n\n```\n{\n "data": [\n {\n "session_id": "string",\n "settled_at": "2026-11-06T07:00:00.000Z",\n "paid_at": "2026-11-06T07:00:00.000Z",\n "currency": "string",\n "amount_total": "1000000",\n "pay_chain": "string",\n "pay_asset": "string",\n "deposit_tx_hash": "string",\n "sweep_tx_hash": "string",\n "fee_amount": "string",\n "net_amount": "string",\n "token_decimals": 0,\n "fee_amount_token": "string",\n "net_amount_token": "string",\n "metadata": {}\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n },\n "summary": {\n "total_count": 0,\n "total_amount_atomic": "1000000",\n "total_fees_atomic": "1000000",\n "total_net_atomic": "1000000"\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/reports/fees` \u2014 Fees report\n\nRows: every session **paid** in the range (by `paid_at`), with the platform fee, newest first. Only the API key\u2019s mode (test or live) is included. With `format=json` (default) you get one page (`limit`/`offset`), `meta.total` and a `summary` over the whole range. With `format=csv` you get the whole range as one RFC 4180 CSV file (`text/csv`, CRLF line endings; `limit`/`offset` are ignored) with the same columns in the same order \u2014 empty cells for null, `metadata` as a JSON string. Amounts are atomic integer strings: `amount_total`, `fee_amount` and `net_amount` are 6-decimal; `fee_amount_token` and `net_amount_token` are in the payment token\u2019s decimals (`token_decimals`) and match the on-chain transfers exactly.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `from` | query | string (date) | No | First day to include, UTC date `YYYY-MM-DD`. Omit for no lower bound. |\n| `to` | query | string (date) | No | Last day to include (the whole day), UTC date `YYYY-MM-DD`. Omit for no upper bound. Must not be before `from`. |\n| `format` | query | "json" \\| "csv" | No | `json` (default) or `csv`. (default `"json"`) |\n| `limit` | query | integer | No | JSON page size, 1\u2013100 (default 50). Ignored for CSV. (1\u2013100, default `50`) |\n| `offset` | query | integer | No | JSON page offset (default 0). Ignored for CSV. (\u2265 0, default `0`) |\n\n**Response `200`** \u2014 One page of rows. `meta` carries total/limit/offset; `summary` totals the whole range. With `format=csv` the body is a CSV file instead.\n\n```\n{\n "data": [\n {\n "session_id": "string",\n "paid_at": "2026-11-06T07:00:00.000Z",\n "currency": "string",\n "amount_total": "1000000",\n "pay_chain": "string",\n "pay_asset": "string",\n "fee_rate": "string",\n "fee_amount": "string",\n "net_amount": "string",\n "token_decimals": 0,\n "fee_amount_token": "string",\n "net_amount_token": "string",\n "metadata": {}\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n },\n "summary": {\n "total_count": 0,\n "total_amount_atomic": "1000000",\n "total_fees_atomic": "1000000",\n "total_net_atomic": "1000000"\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n## Events\n\nThe checkout events BitPal emits, with their webhook deliveries \u2014 for reconciliation and resending.\n\n### `GET /v1/checkout/events` \u2014 List events\n\nCheckout events of the API key\u2019s environment (test or live), newest first \u2014 the same events your webhook endpoints receive, whether or not you have an endpoint. Use it to reconcile missed webhooks. `meta` carries total/limit/offset.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `page` | query | integer | No | Page number, from 1 (default 1). (\u2265 1, default `1`) |\n| `limit` | query | integer | No | Page size, 1\u2013100 (default 50). (1\u2013100, default `50`) |\n| `event_type` | query | string | No | Only this event, e.g. `checkout.session.paid`. (\u2265 1 chars) |\n| `from` | query | string (date-time) | No | Only events at or after this ISO-8601 time (with offset). |\n| `to` | query | string (date-time) | No | Only events at or before this ISO-8601 time (with offset). |\n\n**Response `200`** \u2014 One page of events.\n\n```\n{\n "data": [\n {\n "id": "00000000-0000-4000-8000-000000000000",\n "event_type": "string",\n "aggregate_type": "string",\n "aggregate_id": "string",\n "payload": {},\n "is_test": true,\n "created_at": "2026-11-06T07:00:00.000Z"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/events/{id}` \u2014 Retrieve an event with its deliveries\n\nThe event and every webhook delivery made for it (status, attempts, last HTTP status).\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Event id (uuid) \u2014 the envelope `id`. |\n\n**Response `200`** \u2014 The event and its deliveries.\n\n```\n{\n "data": {\n "event": {\n "id": "00000000-0000-4000-8000-000000000000",\n "event_type": "string",\n "aggregate_type": "string",\n "aggregate_id": "string",\n "payload": {},\n "is_test": true,\n "created_at": "2026-11-06T07:00:00.000Z"\n },\n "deliveries": [\n {\n "id": "00000000-0000-4000-8000-000000000000",\n "webhook_id": "00000000-0000-4000-8000-000000000000",\n "event_id": "00000000-0000-4000-8000-000000000000",\n "event_type": "string",\n "status": "string",\n "attempt_count": 0,\n "last_delivery_status": 0,\n "next_retry_at": "2026-11-06T07:00:00.000Z",\n "replayed_from_event_id": "00000000-0000-4000-8000-000000000000",\n "created_at": "2026-11-06T07:00:00.000Z"\n }\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `POST /v1/checkout/events/{id}/retry` \u2014 Resend an event to subscribed endpoints\n\nQueues the event again for every active endpoint in this environment that subscribes to it \u2014 or only `webhook_id`. The envelope keeps the original `id` and `created_at`, so your dedup still applies. To resend to one endpoint, including one created after the event, you can also use `POST /v1/webhooks/{id}/replay`.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Event id (uuid) \u2014 the envelope `id`. |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`RetryCheckoutEventRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `webhook_id` | string (uuid) | No | Optional: resend only to this endpoint (it must be active and subscribed to the event). |\n\n```\n{}\n```\n\n**Response `200`** \u2014 The queued deliveries.\n\n```\n{\n "data": {\n "message": "string",\n "delivery_ids": [\n "00000000-0000-4000-8000-000000000000"\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n## Sandbox\n\nSimulate a buyer payment in test mode and follow it, without a wallet; inspect webhook events and their deliveries.\n\n### `POST /v1/sandbox/sessions/{id}/simulate` \u2014 Simulate a buyer payment\n\nTest mode only: BitPal sends a testnet deposit to the session for you, so you can test fulfilment and webhooks without a wallet. Scenarios: `exact` (pays the rest \u2014 the session gets paid), `underpay` (half of the rest), `overpay` (the rest + 1 token \u2014 the excess is refunded), `wrong_token` (1 unit of another token on the same chain \u2014 returned; the session stays payable). Issues the deposit address first if needed. A `bp_live_` key gets 403 `SANDBOX_TEST_MODE_ONLY`. Limited to 10 calls per minute and 300 per day per account.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Checkout session id (`cs_\u2026`). |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`SimulateSandboxPaymentRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `scenario` | "exact" \\| "underpay" \\| "overpay" \\| "wrong_token" | Yes | `exact`, `underpay`, `overpay` or `wrong_token`. |\n| `chain` | string | No | Only before the deposit address is issued, for a session with several payment options: the network to pay on (with `token`). (\u2265 1 chars) |\n| `token` | string | No | Goes with `chain`, e.g. `USDC`. (\u2265 1 chars) |\n| `refund_address` | string | No | Buyer refund address, used only when this call issues the deposit address. Omit it and refunds go to BitPal\u2019s test buyer wallet. (1\u2013128 chars) |\n\n```\n{\n "scenario": "exact"\n}\n```\n\n**Response `201`** \u2014 The deposit was sent.\n\n```\n{\n "data": {\n "session_id": "string",\n "scenario": "exact",\n "chain": "string",\n "token": "string",\n "amount": "1000000",\n "decimals": 0,\n "deposit_address": "string",\n "tx_hash": "string"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `404` \xB7 `409` \xB7 `429` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/sandbox/sessions/{id}/progress` \u2014 Follow a test payment\n\nTest mode only. One view of a test session: stage, confirmations while the deposit confirms, deposit and settlement transactions with explorer links, and refunds. Calling it also checks for the deposit right away. Stop polling once `terminal` is true.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Checkout session id (`cs_\u2026`). |\n\n**Response `200`** \u2014 The session\u2019s progress.\n\n```\n{\n "data": {\n "session_id": "string",\n "status": "string",\n "stage": "awaiting_address",\n "terminal": true,\n "chain": "string",\n "token": "string",\n "token_decimals": 0,\n "amount_received": "string",\n "deposit_address": "string",\n "deposit_tx_hash": "string",\n "deposit_tx_url": "https://example.com",\n "sweep_tx_hash": "string",\n "sweep_tx_url": "https://example.com",\n "confirmations": {\n "current": 0,\n "required": 1\n },\n "created_at": "string",\n "paid_at": "string",\n "settled_at": "string",\n "expires_at": "string",\n "refunds": [\n {\n "kind": "string",\n "amount": "1000000",\n "amount_display": "string",\n "token_symbol": "string",\n "tx_hash": "string",\n "tx_url": "https://example.com",\n "created_at": "string"\n }\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/sandbox/events` \u2014 List webhook events with delivery results\n\nWebhook events of the API key\u2019s environment (test or live), newest first, each as the exact body your endpoint receives plus the result per endpoint. Works without any endpoint registered. Filter by `session_id` to follow one test payment.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `session_id` | query | string | No | Only events of this session (`cs_\u2026`). |\n| `limit` | query | integer | No | Max events, 1\u2013100 (default 50). (1\u2013100, default `50`) |\n\n**Response `200`** \u2014 Events with their deliveries.\n\n```\n{\n "data": {\n "events": [\n {\n "envelope": {\n "id": "string",\n "event": "string",\n "created_at": "string",\n "data": {}\n },\n "is_test": true,\n "deliveries": [\n {\n "webhook_id": "00000000-0000-4000-8000-000000000000",\n "endpoint_url": "string",\n "status": "string",\n "attempt_count": 0,\n "last_http_status": 0\n }\n ]\n }\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n## Webhook endpoints\n\nManage where BitPal delivers signed events.\n\n### `POST /v1/webhooks` \u2014 Create a webhook endpoint\n\n**Test API key or console.** A `bp_test_` key can call this for test mode; a `bp_live_` key gets 403 \u2014 manage live endpoints in Console \u2192 Developers \u2192 Webhooks. The response carries the `whsec_\u2026` signing secret \u2014 it is shown only once. With a `bp_test_` key this creates a test-mode endpoint (for CI, or local development behind a tunnel); live endpoints are created in the console. Recorded in your audit log.\n\n**Auth:** Test API key (`bp_test_\u2026`) or console login \u2014 live keys get 403; manage live endpoints in Console \u2192 Developers \u2192 Webhooks\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`CreateWebhookEndpointRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `endpoint_url` | string (uri) | Yes | HTTPS URL that receives events. Private hosts are rejected. |\n| `subscribed_events` | array of ("*" \\| "checkout.session.created" \\| "checkout.session.deposit_detected" \\| "checkout.session.paid" \\| "checkout.session.settled" \\| "checkout.session.expired" \\| "checkout.session.unresolved" \\| "checkout.refund.recorded" \\| "x402.payment.resolved") | Yes | Event names to deliver (see the `webhooks` section), or `*` for every event. Unknown names are rejected with 400. (\u2265 1 items) |\n| `retry_policy` | object | No | `max_retries` caps the total delivery attempts (1\u201320, default 10: 3 right away, then 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h). The schedule itself is fixed; `backoff` is ignored. |\n| `dead_letter_destination` | string (uri) \\| null | No | Optional URL for dead-lettered deliveries. |\n\n```\n{\n "endpoint_url": "https://your.store/webhooks/bitpal",\n "subscribed_events": [\n "checkout.session.paid"\n ]\n}\n```\n\n**Response `201`** \u2014 Endpoint created.\n\n```\n{\n "data": {\n "id": "00000000-0000-4000-8000-000000000000",\n "mode": "TEST",\n "endpoint_url": "string",\n "subscribed_events": [\n "string"\n ],\n "retry_policy": {\n "max_retries": 0,\n "backoff": "exponential"\n },\n "dead_letter_destination": "string",\n "status": "active",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z",\n "secret": "string"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/webhooks` \u2014 List webhook endpoints\n\n**Test API key or console.** A `bp_test_` key can call this for test mode; a `bp_live_` key gets 403 \u2014 manage live endpoints in Console \u2192 Developers \u2192 Webhooks. Endpoints of the caller\u2019s environment and mode (a `bp_test_` key lists test-mode endpoints).\n\n**Auth:** Test API key (`bp_test_\u2026`) or console login \u2014 live keys get 403; manage live endpoints in Console \u2192 Developers \u2192 Webhooks\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `limit` | query | integer | No | 1\u2013100, default `50` |\n| `offset` | query | integer | No | \u2265 0, default `0` |\n\n**Response `200`** \u2014 Endpoints, newest first. `meta` carries total/limit/offset.\n\n```\n{\n "data": [\n {\n "id": "00000000-0000-4000-8000-000000000000",\n "mode": "TEST",\n "endpoint_url": "string",\n "subscribed_events": [\n "string"\n ],\n "retry_policy": {\n "max_retries": 0,\n "backoff": "exponential"\n },\n "dead_letter_destination": "string",\n "status": "active",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \u2014 see [Errors](/docs/md/errors.md).\n\n### `PATCH /v1/webhooks/{id}` \u2014 Update a webhook endpoint\n\n**Test API key or console.** A `bp_test_` key can call this for test mode; a `bp_live_` key gets 403 \u2014 manage live endpoints in Console \u2192 Developers \u2192 Webhooks. Setting `status` to `disabled` removes the endpoint. In the console a `reason_code` is required (otherwise 400 `REASON_CODE_REQUIRED`); with a `bp_test_` key it is optional and a request without it is recorded as `api_request`. Recorded in the audit log.\n\n**Auth:** Test API key (`bp_test_\u2026`) or console login \u2014 live keys get 403; manage live endpoints in Console \u2192 Developers \u2192 Webhooks\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Webhook endpoint id (uuid). |\n\n**Request body** (`UpdateWebhookEndpointRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `endpoint_url` | string (uri) | No | |\n| `subscribed_events` | array of ("*" \\| "checkout.session.created" \\| "checkout.session.deposit_detected" \\| "checkout.session.paid" \\| "checkout.session.settled" \\| "checkout.session.expired" \\| "checkout.session.unresolved" \\| "checkout.refund.recorded" \\| "x402.payment.resolved") | No | \u2265 1 items |\n| `status` | "active" \\| "paused" \\| "disabled" | No | |\n| `retry_policy` | object | No | |\n| `dead_letter_destination` | string (uri) \\| null | No | |\n| `reason_code` | "user_request" \\| "security_concern" \\| "rotation" \\| "no_longer_needed" \\| "other" | No | When `status` is `disabled`: one of user_request, security_concern, rotation, no_longer_needed, other. Required in the console; optional with a test API key (recorded as `api_request`). |\n| `reason_note` | string | No | Optional free text (\u2264 500 chars) stored with the audit record. (\u2264 500 chars) |\n\n**Response `200`** \u2014 The updated endpoint.\n\n```\n{\n "data": {\n "id": "00000000-0000-4000-8000-000000000000",\n "mode": "TEST",\n "endpoint_url": "string",\n "subscribed_events": [\n "string"\n ],\n "retry_policy": {\n "max_retries": 0,\n "backoff": "exponential"\n },\n "dead_letter_destination": "string",\n "status": "active",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `POST /v1/webhooks/{id}/rotate-secret` \u2014 Rotate the signing secret\n\n**Test API key or console.** A `bp_test_` key can call this for test mode; a `bp_live_` key gets 403 \u2014 manage live endpoints in Console \u2192 Developers \u2192 Webhooks. Takes effect immediately: the old secret stops working and the new one is returned once \u2014 update your verifier. In the console a `reason_code` is required (400 `REASON_CODE_REQUIRED` if missing); with a `bp_test_` key it is optional and recorded as `api_request`. Recorded in the audit log.\n\n**Auth:** Test API key (`bp_test_\u2026`) or console login \u2014 live keys get 403; manage live endpoints in Console \u2192 Developers \u2192 Webhooks\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Webhook endpoint id (uuid). |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`RotateWebhookSecretRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `reason_code` | "user_request" \\| "security_concern" \\| "rotation" \\| "no_longer_needed" \\| "other" | No | One of user_request, security_concern, rotation, no_longer_needed, other. Required in the console; optional with a test API key. |\n| `reason_note` | string | No | Optional free text (\u2264 500 chars) stored with the audit record. (\u2264 500 chars) |\n\n```\n{\n "reason_code": "rotation"\n}\n```\n\n**Response `200`** \u2014 Endpoint with the new secret.\n\n```\n{\n "data": {\n "id": "00000000-0000-4000-8000-000000000000",\n "mode": "TEST",\n "endpoint_url": "string",\n "subscribed_events": [\n "string"\n ],\n "retry_policy": {\n "max_retries": 0,\n "backoff": "exponential"\n },\n "dead_letter_destination": "string",\n "status": "active",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z",\n "secret": "string"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `POST /v1/webhooks/{id}/replay` \u2014 Resend a past event\n\n**Test API key or console.** A `bp_test_` key can call this for test mode; a `bp_live_` key gets 403 \u2014 manage live endpoints in Console \u2192 Developers \u2192 Webhooks. Queues one past event for delivery to this endpoint \u2014 including events from before the endpoint existed, so a newly registered endpoint can catch up. `event_id` is the envelope `id` of the event (unchanged, so your dedup still applies); the body is the original `data`, signed with the endpoint\u2019s current secret and delivered with the usual retries. Only events of the caller\u2019s environment (test or live) can be resent. Recorded in the audit log.\n\n**Auth:** Test API key (`bp_test_\u2026`) or console login \u2014 live keys get 403; manage live endpoints in Console \u2192 Developers \u2192 Webhooks\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Webhook endpoint id (uuid). |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`ReplayWebhookEventRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `event_id` | string (uuid) | Yes | Event id (the envelope `id`, uuid) \u2014 from an earlier delivery, or from the MCP `list_test_events` tool in test mode. |\n\n```\n{\n "event_id": "1ff63374-9417-4f87-850d-e869a3bd2dbe"\n}\n```\n\n**Response `201`** \u2014 The queued delivery.\n\n```\n{\n "data": {\n "id": "00000000-0000-4000-8000-000000000000",\n "webhook_id": "00000000-0000-4000-8000-000000000000",\n "event_id": "00000000-0000-4000-8000-000000000000",\n "event_type": "string",\n "status": "string",\n "attempt_count": 0,\n "replayed_from_event_id": "00000000-0000-4000-8000-000000000000",\n "created_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `POST /v1/developers/webhooks/test` \u2014 Send a test event\n\nPosts one signed sample event to an endpoint (no retries). The envelope id is prefixed `test_`, `data.test` is true and the request carries `X-Webhook-Test: true`.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`SendTestWebhookRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `webhook_id` | string (uuid) | Yes | Endpoint id. |\n| `event_type` | string | Yes | Event name to put in the envelope, e.g. checkout.session.paid. (1\u2013200 chars) |\n| `payload` | map | No | Custom `data` object. Defaults to a sample message. |\n\n```\n{\n "webhook_id": "00000000-0000-4000-8000-000000000000",\n "event_type": "checkout.session.paid"\n}\n```\n\n**Response `200`** \u2014 Delivery result.\n\n```\n{\n "data": {\n "delivery_id": "00000000-0000-4000-8000-000000000000",\n "webhook_id": "00000000-0000-4000-8000-000000000000",\n "endpoint_url": "string",\n "event_type": "string",\n "http_status": 0,\n "response_body": "string",\n "error": "string",\n "duration_ms": 0,\n "test": true,\n "sent_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `404` \xB7 `409` \u2014 see [Errors](/docs/md/errors.md).\n\n## Webhook events\n\nEnvelope: `{ id, event, created_at, data }`, signed with `X-Webhook-Signature-256`. Verification and retries: [Webhooks](/docs/md/webhooks.md).\n\n| Event | Fires when | `data` fields |\n| --- | --- | --- |\n| `checkout.session.created` | A session is created. | `session_id`, `metadata`, `amount_total`, `currency`, `late` |\n| `checkout.session.deposit_detected` | Funds seen on-chain, awaiting confirmations. Optional \u2014 skipped when the first time BitPal sees the deposit it is already confirmed; then `checkout.session.paid` (or `unresolved`) arrives with no `deposit_detected` before it. Do not wait for it. | `session_id`, `metadata`, `amount_total`, `currency`, `late`, `tx_hash`, `amount_received`, `received_token` |\n| `checkout.session.paid` | Confirmed \u2014 safe to fulfil. `late: true` if it arrived after the deadline. | `session_id`, `metadata`, `amount_total`, `currency`, `late`, `tx_hash` |\n| `checkout.session.settled` | Funds swept to your payout wallet. | `session_id`, `metadata`, `amount_total`, `currency`, `late`, `sweep_tx_hash` |\n| `checkout.session.unresolved` | Underpaid, wrong token, or a confirmed deposit reverted by a reorg (`reason`). | `session_id`, `metadata`, `amount_total`, `currency`, `late`, `reason`, `amount_expected`, `amount_received`, `received_token`, `tx_hash` |\n| `checkout.session.expired` | Deadline passed without full payment. Timing: a session that never got a deposit address expires at `expires_at` (applied within about 5 minutes, or at once when the session is read). Once a deposit address was issued, this event is sent shortly after the 90-minute grace that follows `expires_at` ends (`expires_at` + 90 min), because a transfer already in flight may still confirm. If you cannot hold stock that long, release it yourself at `expires_at`; a full payment can still arrive later as `checkout.session.paid` with `late: true` (unless the session was created with `late_payments: "refund"`). | `session_id`, `metadata`, `amount_total`, `currency`, `late`, `reason`, `expired_at`, `source`, `refund_tx_hash` |\n| `checkout.refund.recorded` | A refund was made on-chain (`kind`): automatic, or recorded by you in the console (`merchant`). | `session_id`, `metadata`, `kind`, `amount`, `token`, `tx_hash`, `refund_address`, `from_address`, `reason_code` |\n| `x402.payment.resolved` | An x402 call that settled with `503` reached its outcome. Only fires for those \u2014 the normal path answers inline. | `payment_id`, `status`, `resource`, `amount`, `asset`, `chain`, `tx_hash`, `payer`, `resolved_by` |'
|
|
4070
|
+
markdown: '# API reference\n\nEvery public endpoint, generated from the OpenAPI 3.1.0 spec (version 1.0.0). Download it: [openapi.json](/docs/openapi.json).\n\n## Basics\n\n| Topic | Details |\n| --- | --- |\n| Base URL | `https://api.bitpal.io` \u2014 test and live share the host; the API key prefix selects the mode. |\n| Auth | `Authorization: Bearer bp_test_\u2026` (test) or `bp_live_\u2026` (live). Keep keys on your server. |\n| Format | JSON. Success: `{ "data": \u2026 }` (authenticated endpoints add `meta: { mode, requestId }`). Amounts are strings. |\n| Errors | `{ "error", "code", "retryable", "trace_id", "details"? }` \u2014 `error` is the human-readable message; branch on `code`. See [Errors](/docs/md/errors.md). |\n| Idempotency | `Idempotency-Key` (8\u2013128 chars) is honoured on every authenticated merchant `POST`; endpoints that do not enforce it say so. |\n\n## Checkout sessions\n\nCreate a session per order, send the buyer to the hosted page (or build your own UI), and track it.\n\n### `POST /v1/checkout/sessions` \u2014 Create a checkout session\n\nCreates a session for one order and returns the hosted `checkout_url`. Line items take either `amount` (plain decimal string, e.g. "29") or `amount_atomic` (6-decimal integer string), or reference a `price_id`. **Minimum amount:** the session total (sum of `amount \xD7 quantity`) must be at least 1.00 in the session currency (USDC/USDT: $1, JPYC: \xA51); a lower total returns 400 `CHECKOUT_AMOUNT_TOO_LOW`. Some networks also have a higher per-network minimum (Tron: currently $300, testnet included; JPYC: \xA5100) \u2014 a network whose minimum is above the total is not offered to the buyer, and requesting a deposit address on it returns 400 `CHECKOUT_BELOW_MIN_PAYMENT`. **Payout wallet:** you need a payout wallet registered in the session\'s mode (test/live). With `pay_chain`, a wallet for that network\'s type (EVM, Tron or Solana) is required. With `allowed_pay_chains` / `allowed_assets`, at least one listed network must have a wallet of its type, and the buyer is only offered networks you can receive on. Without any network restriction, any payout wallet is enough: with an EVM wallet the buyer is offered Base USDC as before; without one, the networks of the wallet types you registered (if the total is below the minimum of all of them, 400 `CHECKOUT_BELOW_MIN_PAYMENT`). No usable payout wallet returns 400 `SELLER_WALLET_NOT_CONFIGURED`.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`CreateCheckoutSessionRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `line_items` | array of (object \\| any \\| any) | Yes | Raw items (`name`, `currency`, `amount` XOR `amount_atomic`) or price items (`price_id`). Do not mix the two kinds. The total (sum of `amount \xD7 quantity`) must be at least 1.00 in the session currency, otherwise 400 `CHECKOUT_AMOUNT_TOO_LOW`. (\u2265 1 items) |\n| `currency` | string | No | Session currency. Defaults to the line items\u2019 currency. (\u2265 1 chars) |\n| `success_url` | string (uri) | No | Where the buyer returns after paying. `{CHECKOUT_SESSION_ID}` is replaced with the session id. (\u2264 2048 chars) |\n| `cancel_url` | string (uri) | No | Where the buyer goes when cancelling. (\u2264 2048 chars) |\n| `customer_email` | string (email) | No | Buyer email. Pre-fills the receipt form on the payment completion screen; no receipt is sent automatically \u2014 the buyer requests it there. |\n| `metadata` | map | No | Your key/value data, echoed back on the session and in webhooks (`data.metadata`). Reserved keys BitPal writes itself (`_bitpal`, `payer_email`, `allowed_pay_chains`, `allowed_assets`) are not returned \u2014 do not use them. |\n| `expires_in_minutes` | integer | No | Payment window in minutes (1\u201360). Default 15. (1\u201360, default `15`) |\n| `tolerance_percent` | string | No | Deprecated. Underpayments are never accepted \u2014 a short payment becomes `unresolved_underpaid` and can be topped up to the same address. Only `"0"` is accepted; any other value returns 400. |\n| `pay_chain` | string | No | Fix the payment network: CAIP-2 (`eip155:84532`) or a short name (`base`), which resolves to the testnet in test mode and the mainnet in live mode. Mutually exclusive with `allowed_pay_chains`. |\n| `allowed_pay_chains` | array of string | No | Networks the buyer may choose from (CAIP-2 or short names, resolved like `pay_chain`). (1\u20138 items) |\n| `allowed_assets` | array of object | No | Allowed (chain, token) cells. Takes precedence over `allowed_pay_chains`. (1\u201316 items) |\n| `late_payments` | "accept" \\| "refund" | No | What to do with a full payment that arrives after the payment deadline. `accept` (default): accepted as a late payment for up to 7 days (`checkout.session.paid` with `late: true`). `refund`: never accepted \u2014 it is refunded automatically to the buyer about 7 days after the deadline (refund kind `late_deposit`); the non-custodial deposit contract does not allow an earlier refund. |\n\n```\n{\n "line_items": [\n {\n "name": "Pro plan",\n "amount": "29",\n "currency": "USDC"\n }\n ],\n "pay_chain": "eip155:84532",\n "expires_in_minutes": 30,\n "success_url": "https://your.store/thanks?session={CHECKOUT_SESSION_ID}"\n}\n```\n\n**Response `201`** \u2014 Session created.\n\n```\n{\n "data": {\n "id": "cs_Q2FxKhomCEju_OqvKfrQsTOh",\n "checkout_url": "https://bitpal.io/pay/cs_Q2FxKhomCEju_OqvKfrQsTOh?token=<session_token>",\n "session_token": "<session_token>",\n "status": "created",\n "amount_total": "29000000",\n "currency": "USDC",\n "expires_at": "2026-11-06T07:58:28.776Z",\n "tax": {\n "tax_amount": "0",\n "total_with_tax": "29000000"\n }\n },\n "meta": {\n "mode": "TEST",\n "requestId": "ec091faf-\u2026"\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `409` \xB7 `422` \xB7 `429` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/sessions` \u2014 List checkout sessions\n\nNewest first, in the API key\u2019s mode (test/live).\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `status` | query | string | No | Filter by status, or `succeeded` for all successful states. |\n| `limit` | query | integer | No | 1\u2013100, default `50` |\n| `offset` | query | integer | No | \u2265 0, default `0` |\n\n**Response `200`** \u2014 Sessions.\n\n```\n{\n "data": [\n {\n "id": "string",\n "status": "string",\n "mode": "TEST",\n "amount_total": "1000000",\n "amount_received": "string",\n "currency": "string",\n "pay_chain": "string",\n "pay_asset": "string",\n "token_decimals": 0,\n "payment_method": "string",\n "payer_wallet": "string",\n "seller_wallet": "string",\n "tolerance_percent": "string",\n "late_payments": "accept",\n "deposit_address": "string",\n "refund_address": "string",\n "received_token": "string",\n "received_token_symbol": "string",\n "fee_amount": "string",\n "fee_rate": "string",\n "net_amount": "string",\n "fee_amount_token": "string",\n "net_amount_token": "string",\n "tx_hash": "string",\n "deposit_tx_hash": "string",\n "sweep_tx_hash": "string",\n "refund_status": "none",\n "success_url": "string",\n "cancel_url": "string",\n "metadata": {},\n "payment_source": "string",\n "payment_link_id": "string",\n "expires_at": "2026-11-06T07:00:00.000Z",\n "paid_at": "2026-11-06T07:00:00.000Z",\n "settled_at": "2026-11-06T07:00:00.000Z",\n "created_at": "2026-11-06T07:00:00.000Z",\n "raw_status": "string"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/sessions/{id}` \u2014 Retrieve a checkout session\n\nServer-side source of truth for an order, together with webhooks.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Checkout session id (`cs_\u2026`). |\n\n**Response `200`** \u2014 The session.\n\n```\n{\n "data": {\n "id": "string",\n "status": "string",\n "mode": "TEST",\n "amount_total": "1000000",\n "amount_received": "string",\n "currency": "string",\n "pay_chain": "string",\n "pay_asset": "string",\n "token_decimals": 0,\n "payment_method": "string",\n "payer_wallet": "string",\n "seller_wallet": "string",\n "tolerance_percent": "string",\n "late_payments": "accept",\n "deposit_address": "string",\n "refund_address": "string",\n "received_token": "string",\n "received_token_symbol": "string",\n "fee_amount": "string",\n "fee_rate": "string",\n "net_amount": "string",\n "fee_amount_token": "string",\n "net_amount_token": "string",\n "tx_hash": "string",\n "deposit_tx_hash": "string",\n "sweep_tx_hash": "string",\n "refund_status": "none",\n "success_url": "string",\n "cancel_url": "string",\n "metadata": {},\n "payment_source": "string",\n "payment_link_id": "string",\n "expires_at": "2026-11-06T07:00:00.000Z",\n "paid_at": "2026-11-06T07:00:00.000Z",\n "settled_at": "2026-11-06T07:00:00.000Z",\n "created_at": "2026-11-06T07:00:00.000Z",\n "line_items": [\n {\n "id": "string",\n "name": "string",\n "amount": "1000000",\n "currency": "string",\n "quantity": 0,\n "price_id": "string",\n "product_id": "string",\n "created_at": "2026-11-06T07:00:00.000Z"\n }\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/sessions/{id}/status` \u2014 Get session status\n\nLightweight status for polling from your server. Requires your API key (N13 \u2014 it is not a public endpoint). Buyer-facing pages should use `GET /v1/checkout/sessions/{id}/public`.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Checkout session id (`cs_\u2026`). |\n\n**Response `200`** \u2014 Status.\n\n```\n{\n "data": {\n "status": "awaiting_deposit",\n "paid_at": null\n }\n}\n```\n\n**Errors:** `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/sessions/{id}/public` \u2014 Get the public session view\n\nBuyer-facing view used by the checkout page: payment options, deposit address once issued, refunds. No authentication; CORS-open.\n\n**Auth:** None \u2014 public endpoint\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Checkout session id (`cs_\u2026`). |\n\n**Response `200`** \u2014 Public view.\n\n```\n{\n "data": {\n "id": "string",\n "status": "string",\n "token_decimals": 0,\n "pay_amount": "string",\n "mode": "TEST",\n "chain_id": 0,\n "token_address": "string",\n "payment_options": [\n {\n "caip2": "string",\n "chain_id": 0,\n "token_symbol": "string",\n "token_address": "string",\n "decimals": 0,\n "amount_atomic": "string",\n "available": true,\n "unavailable_reason": "coming_soon"\n }\n ],\n "currency": "string",\n "amount_total": "1000000",\n "amount_received": "string",\n "chain": "string",\n "tx_hash": "string",\n "release_tx_hash": "string",\n "expires_at": "2026-11-06T07:00:00.000Z",\n "success_url": "string",\n "cancel_url": "string",\n "fee_amount": "string",\n "fee_rate": "string",\n "net_amount": "string",\n "tax_amount": null,\n "tax_rate": null,\n "pay_chain": "string",\n "pay_asset": "string",\n "deposit_address": "string",\n "deposit_owner": "string",\n "payment_deadline_at": "2026-11-06T07:00:00.000Z",\n "payment_method": "string",\n "payer_wallet": "string",\n "seller_wallet": "string",\n "tolerance_percent": "string",\n "paid_at": "2026-11-06T07:00:00.000Z",\n "settled_at": "2026-11-06T07:00:00.000Z",\n "created_at": "2026-11-06T07:00:00.000Z",\n "merchant_name": "string",\n "line_items": [\n {\n "name": "string",\n "amount": "1000000",\n "currency": "string",\n "quantity": 0\n }\n ],\n "payment_source": "string",\n "refunds": [\n {\n "kind": "string",\n "amount": "string",\n "token_symbol": "string",\n "decimals": 0,\n "tx_hash": "string",\n "caip2": "string"\n }\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `POST /v1/checkout/sessions/{id}/deposit-address` \u2014 Issue the deposit address\n\nFixes the buyer\u2019s network, token and refund address, and returns the one-time deposit address with the exact amount. Calling again returns the same address and keeps the first refund address. Requires the session token (header `X-Session-Token` or body `session_token`).\n\n**Auth:** Session token \u2014 `X-Session-Token` header or `session_token` body field (no API key)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Checkout session id (`cs_\u2026`). |\n| `Idempotency-Key` | header | string | No | Accepted but not yet enforced on this endpoint \u2014 retries are not de-duplicated by key. (8\u2013128 chars) |\n| `X-Session-Token` | header | string | No | Session token from the create-session response. Alternative to the `session_token` body field. |\n\n**Request body** (`IssueDepositAddressRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `session_token` | string | No | From the create-session response. May be sent as the `X-Session-Token` header instead. |\n| `refundAddress` | string | Yes | Buyer refund address in the chosen network\u2019s format (EVM 0x\u2026, Tron T\u2026, Solana base58). Cannot change later. |\n| `chain` | string | No | CAIP-2 or short name of the chosen network. Required for multi-option sessions. (\u2265 1 chars) |\n| `token` | string | No | Token symbol of the chosen asset, e.g. USDC. Required for multi-option sessions. (\u2265 1 chars) |\n\n```\n{\n "session_token": "<session_token>",\n "refundAddress": "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720",\n "chain": "eip155:84532",\n "token": "USDC"\n}\n```\n\n**Response `200`** \u2014 Deposit address.\n\n```\n{\n "data": {\n "sessionId": "cs_\u2026",\n "chain": "eip155:84532",\n "depositAddress": "0x6b19e871c934aB52775B6712e110f3e4b0626347",\n "expectedToken": "0x943b94dacf1507f2658f1cfe103ac36ebc13ef65",\n "expectedAmount": "29000000",\n "feeAmount": "435000",\n "refundAddress": "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720",\n "deadline": "1793951938",\n "expiresAt": "2026-11-06T07:58:28.776Z",\n "depositOwner": null\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `404` \xB7 `409` \xB7 `410` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/sessions/{id}/refunds` \u2014 List refunds for a session\n\nRefunds recorded for this session: automatic on-chain refunds from the deposit address (overpayment, expired underpayment, wrong token, late deposit) and refunds you sent from your own wallet and recorded in the console (`kind: merchant`). There is no API to make a refund \u2014 BitPal is non-custodial; refund a completed payment from Console \u2192 Sessions \u2192 session \u2192 Refund.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Checkout session id (`cs_\u2026`). |\n\n**Response `200`** \u2014 Refunds.\n\n```\n{\n "data": [\n {\n "id": "string",\n "kind": "string",\n "token": "string",\n "token_symbol": "string",\n "amount": "string",\n "amount_display": "string",\n "decimals": 0,\n "refund_address": "string",\n "tx_hash": "string",\n "caip2": "string",\n "block_number": "string",\n "created_at": "2026-11-06T07:00:00.000Z",\n "from_address": "string",\n "reason_code": "string",\n "note": "string",\n "explorer_url": "string"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/fee-preview` \u2014 Preview the fee for an amount\n\nPass exactly one of `amount` or `amount_atomic`. The response is always atomic.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `amount` | query | string | No | Decimal string, e.g. "100". |\n| `amount_atomic` | query | string | No | 6-decimal integer string, e.g. "100000000". |\n\n**Response `200`** \u2014 Fee breakdown.\n\n```\n{\n "data": {\n "amount": "100000000",\n "fee_rate": "0.015",\n "fee_amount": "1500000",\n "net_amount": "98500000"\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n## Products & prices\n\nCatalog objects. Prices are immutable.\n\n### `POST /v1/checkout/products` \u2014 Create a product\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`CreateProductRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `name` | string | Yes | 1\u2013200 chars |\n| `description` | string | No | \u2264 2000 chars |\n| `image_url` | string (uri) | No | \u2264 2048 chars |\n| `metadata` | map | No | |\n\n```\n{\n "name": "Pro plan"\n}\n```\n\n**Response `201`** \u2014 Product created.\n\n```\n{\n "data": {\n "id": "string",\n "name": "string",\n "description": "string",\n "image_url": "string",\n "status": "active",\n "metadata": {},\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/products` \u2014 List products\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `status` | query | "active" \\| "inactive" | No | |\n| `limit` | query | integer | No | 1\u2013100, default `50` |\n| `offset` | query | integer | No | \u2265 0, default `0` |\n\n**Response `200`** \u2014 Products.\n\n```\n{\n "data": [\n {\n "id": "string",\n "name": "string",\n "description": "string",\n "image_url": "string",\n "status": "active",\n "metadata": {},\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/products/{id}` \u2014 Retrieve a product\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Product id (`prod_\u2026`). |\n\n**Response `200`** \u2014 The product.\n\n```\n{\n "data": {\n "id": "string",\n "name": "string",\n "description": "string",\n "image_url": "string",\n "status": "active",\n "metadata": {},\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `PATCH /v1/checkout/products/{id}` \u2014 Update a product\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Product id (`prod_\u2026`). |\n\n**Request body** (`UpdateProductRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `name` | string | No | 1\u2013200 chars |\n| `description` | string \\| null | No | \u2264 2000 chars |\n| `image_url` | string (uri) \\| null | No | \u2264 2048 chars |\n| `metadata` | map | No | |\n| `status` | "active" \\| "inactive" | No | |\n\n**Response `200`** \u2014 The updated product.\n\n```\n{\n "data": {\n "id": "string",\n "name": "string",\n "description": "string",\n "image_url": "string",\n "status": "active",\n "metadata": {},\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `POST /v1/checkout/products/{id}/prices` \u2014 Create a price\n\nPrices are immutable: to change an amount, create a new price. Pass exactly one of `amount` (decimal string) or `amount_atomic` (6-decimal integer string). The currency is derived from `token_symbol`.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Product id (`prod_\u2026`). |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`CreatePriceRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `amount` | string | No | Decimal string, e.g. "29". Mutually exclusive with `amount_atomic`. |\n| `amount_atomic` | string | No | 6-decimal integer string ("29000000" = 29). |\n| `token_symbol` | "USDC" \\| "USDT" \\| "JPYC" | No | The price\u2019s asset. The currency is derived from it. (default `"USDC"`) |\n| `billing_type` | "one_time" | No | Only `one_time` for now. (default `"one_time"`) |\n| `nickname` | string | No | Display name. (\u2264 200 chars) |\n| `lookup_key` | string | No | Your own stable key for this price. (1\u2013200 chars) |\n\n```\n{\n "amount": "29",\n "token_symbol": "USDC"\n}\n```\n\n**Response `201`** \u2014 Price created.\n\n```\n{\n "data": {\n "id": "string",\n "product_id": "string",\n "amount_atomic": "1000000",\n "currency": "string",\n "token_symbol": "string",\n "billing_type": "one_time",\n "nickname": "string",\n "lookup_key": "string",\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/products/{id}/prices` \u2014 List a product\u2019s prices\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Product id (`prod_\u2026`). |\n| `limit` | query | integer | No | 1\u2013100, default `50` |\n| `offset` | query | integer | No | \u2265 0, default `0` |\n\n**Response `200`** \u2014 Prices.\n\n```\n{\n "data": [\n {\n "id": "string",\n "product_id": "string",\n "amount_atomic": "1000000",\n "currency": "string",\n "token_symbol": "string",\n "billing_type": "one_time",\n "nickname": "string",\n "lookup_key": "string",\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/prices/{id}` \u2014 Retrieve a price\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Price id (`price_\u2026`). |\n\n**Response `200`** \u2014 The price.\n\n```\n{\n "data": {\n "id": "string",\n "product_id": "string",\n "amount_atomic": "1000000",\n "currency": "string",\n "token_symbol": "string",\n "billing_type": "one_time",\n "nickname": "string",\n "lookup_key": "string",\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `PATCH /v1/checkout/prices/{id}` \u2014 Update a price\u2019s nickname\n\nOnly `nickname` can change. Sending an amount/currency field returns `PRICE_IMMUTABLE_FIELD`.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Price id (`price_\u2026`). |\n\n**Request body** (`UpdatePriceRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `nickname` | string \\| null | No | \u2264 200 chars |\n\n**Response `200`** \u2014 The updated price.\n\n```\n{\n "data": {\n "id": "string",\n "product_id": "string",\n "amount_atomic": "1000000",\n "currency": "string",\n "token_symbol": "string",\n "billing_type": "one_time",\n "nickname": "string",\n "lookup_key": "string",\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n## Payment links\n\nShareable hosted URLs for a price.\n\n### `POST /v1/checkout/payment-links` \u2014 Create a payment link\n\nA shareable hosted URL for one price. The link \u2194 price binding is fixed after creation.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`CreatePaymentLinkRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `price_id` | string | Yes | The price this link sells (`price_\u2026`). |\n| `title` | string | No | 1\u2013200 chars |\n| `description` | string | No | \u2264 2000 chars |\n| `collect_email` | boolean | No | Ask the buyer for an email. Default true. (default `true`) |\n| `email_required` | boolean | No | Require the email: the hosted link page asks for it and no checkout is created without it. Default true; needs `collect_email`. (default `true`) |\n| `allowed_pay_chains` | array of string | No | Networks the buyer may choose from (CAIP-2 or short names). (1\u20138 items) |\n| `allowed_assets` | array of object | No | Allowed (chain, token) cells. Takes precedence over `allowed_pay_chains`. (1\u201316 items) |\n| `success_url` | string (uri) | No | \u2264 2048 chars |\n| `cancel_url` | string (uri) | No | \u2264 2048 chars |\n| `max_uses` | integer | No | Stop accepting payments after this many paid uses. (\u2265 1) |\n| `expires_at` | string (date-time) | No | ISO-8601 time after which the link stops working. |\n| `metadata` | map | No | |\n\n```\n{\n "price_id": "price_\u2026",\n "allowed_pay_chains": [\n "eip155:8453"\n ]\n}\n```\n\n**Response `201`** \u2014 Link created.\n\n```\n{\n "data": {\n "id": "string",\n "share_token": "string",\n "price_id": "string",\n "title": "string",\n "description": "string",\n "collect_email": true,\n "email_required": true,\n "allowed_pay_chains": [\n "string"\n ],\n "allowed_assets": [\n {\n "caip2": "string",\n "token": "string"\n }\n ],\n "success_url": "string",\n "cancel_url": "string",\n "status": "active",\n "max_uses": 0,\n "paid_use_count": 0,\n "expires_at": "2026-11-06T07:00:00.000Z",\n "metadata": {},\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/payment-links` \u2014 List payment links\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `status` | query | "active" \\| "inactive" | No | |\n| `limit` | query | integer | No | 1\u2013100, default `50` |\n| `offset` | query | integer | No | \u2265 0, default `0` |\n\n**Response `200`** \u2014 Links.\n\n```\n{\n "data": [\n {\n "id": "string",\n "share_token": "string",\n "price_id": "string",\n "title": "string",\n "description": "string",\n "collect_email": true,\n "email_required": true,\n "allowed_pay_chains": [\n "string"\n ],\n "allowed_assets": [\n {\n "caip2": "string",\n "token": "string"\n }\n ],\n "success_url": "string",\n "cancel_url": "string",\n "status": "active",\n "max_uses": 0,\n "paid_use_count": 0,\n "expires_at": "2026-11-06T07:00:00.000Z",\n "metadata": {},\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/payment-links/{id}` \u2014 Retrieve a payment link\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Payment link id (`plink_\u2026`). |\n\n**Response `200`** \u2014 The link, with product/price summary fields.\n\n```\n{\n "data": {\n "id": "string",\n "share_token": "string",\n "price_id": "string",\n "title": "string",\n "description": "string",\n "collect_email": true,\n "email_required": true,\n "allowed_pay_chains": [\n "string"\n ],\n "allowed_assets": [\n {\n "caip2": "string",\n "token": "string"\n }\n ],\n "success_url": "string",\n "cancel_url": "string",\n "status": "active",\n "max_uses": 0,\n "paid_use_count": 0,\n "expires_at": "2026-11-06T07:00:00.000Z",\n "metadata": {},\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `PATCH /v1/checkout/payment-links/{id}` \u2014 Update a payment link\n\nOperational settings and active \u2194 inactive. The price cannot change \u2014 create a new link instead. Deactivating (`status: "inactive"`) accepts an optional `reason_code` (`user_request`, `security_concern`, `rotation`, `no_longer_needed` or `other`, plus an optional `reason_note`); with an API key, a request without it is recorded as `api_request` (the console always asks for a reason). Reactivating accepts the same fields optionally. Every status change is recorded in your audit log with its reason.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Payment link id (`plink_\u2026`). |\n\n**Request body** (`UpdatePaymentLinkRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `title` | string \\| null | No | 1\u2013200 chars |\n| `description` | string \\| null | No | \u2264 2000 chars |\n| `collect_email` | boolean | No | |\n| `email_required` | boolean | No | |\n| `allowed_pay_chains` | array of string \\| null | No | 1\u20138 items |\n| `allowed_assets` | array of object \\| null | No | 1\u201316 items |\n| `success_url` | string (uri) \\| null | No | \u2264 2048 chars |\n| `cancel_url` | string (uri) \\| null | No | \u2264 2048 chars |\n| `status` | "active" \\| "inactive" | No | |\n| `max_uses` | integer \\| null | No | \u2265 1 |\n| `expires_at` | string (date-time) \\| null | No | |\n| `metadata` | map | No | |\n| `reason_code` | "user_request" \\| "security_concern" \\| "rotation" \\| "no_longer_needed" \\| "other" | No | |\n| `reason_note` | string | No | \u2264 500 chars |\n\n**Response `200`** \u2014 The updated link.\n\n```\n{\n "data": {\n "id": "string",\n "share_token": "string",\n "price_id": "string",\n "title": "string",\n "description": "string",\n "collect_email": true,\n "email_required": true,\n "allowed_pay_chains": [\n "string"\n ],\n "allowed_assets": [\n {\n "caip2": "string",\n "token": "string"\n }\n ],\n "success_url": "string",\n "cancel_url": "string",\n "status": "active",\n "max_uses": 0,\n "paid_use_count": 0,\n "expires_at": "2026-11-06T07:00:00.000Z",\n "metadata": {},\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `404` \xB7 `409` \u2014 see [Errors](/docs/md/errors.md).\n\n## Customers\n\nBuyers who paid you (created automatically from paid sessions) and their recent payments. Scoped to the current mode.\n\n### `POST /v1/checkout/customers` \u2014 Create a customer\n\nCustomers are also created automatically from paid sessions (by payer wallet, or by email when there is no wallet). A wallet address can exist once per mode \u2014 a duplicate returns 409 `CUSTOMER_ALREADY_EXISTS`.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `Idempotency-Key` | header | string | No | Accepted but not yet enforced on this endpoint \u2014 retries are not de-duplicated by key. (8\u2013128 chars) |\n\n**Request body** (`CreateCustomerRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `wallet_address` | string | Yes | Payer wallet (0x\u2026 / T\u2026 / base58). Unique per account and mode. (1\u2013128 chars) |\n| `email` | string (email) \\| null | No | \u2264 320 chars |\n| `name` | string \\| null | No | \u2264 200 chars |\n| `phone` | string \\| null | No | \u2264 50 chars |\n| `country` | string \\| null | No | Free text; an ISO 3166-1 alpha-2 code such as "US" is recommended. (\u2264 100 chars) |\n| `metadata` | map | No | |\n\n```\n{\n "wallet_address": "0x1111111111111111111111111111111111111111",\n "email": "buyer@example.com"\n}\n```\n\n**Response `201`** \u2014 Customer created.\n\n```\n{\n "data": {\n "id": "string",\n "wallet_address": "string",\n "email": "string",\n "name": "string",\n "phone": "string",\n "country": "string",\n "metadata": {},\n "status": "active",\n "total_spent": "1000000",\n "transaction_count": 0,\n "first_payment_at": "2026-11-06T07:00:00.000Z",\n "last_payment_at": "2026-11-06T07:00:00.000Z",\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `409` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/customers` \u2014 List customers\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `limit` | query | integer | No | 1\u2013100, default `20` |\n| `offset` | query | integer | No | \u2265 0 |\n| `page` | query | integer | No | \u2265 1 |\n| `search` | query | string | No | 1\u2013200 chars |\n| `sort` | query | "created_at" \\| "email" \\| "name" \\| "wallet_address" \\| "total_spent" \\| "transaction_count" | No | default `"created_at"` |\n| `order` | query | "asc" \\| "desc" | No | default `"desc"` |\n\n**Response `200`** \u2014 Customers in the current mode, with `meta.total` for paging.\n\n```\n{\n "data": [\n {\n "id": "string",\n "wallet_address": "string",\n "email": "string",\n "name": "string",\n "phone": "string",\n "country": "string",\n "metadata": {},\n "status": "active",\n "total_spent": "1000000",\n "transaction_count": 0,\n "first_payment_at": "2026-11-06T07:00:00.000Z",\n "last_payment_at": "2026-11-06T07:00:00.000Z",\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/customers/{id}` \u2014 Retrieve a customer\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Customer id (uuid). |\n\n**Response `200`** \u2014 The customer and their recent payments.\n\n```\n{\n "data": {\n "customer": {\n "id": "string",\n "wallet_address": "string",\n "email": "string",\n "name": "string",\n "phone": "string",\n "country": "string",\n "metadata": {},\n "status": "active",\n "total_spent": "1000000",\n "transaction_count": 0,\n "first_payment_at": "2026-11-06T07:00:00.000Z",\n "last_payment_at": "2026-11-06T07:00:00.000Z",\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "recent_payments": [\n {}\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `PATCH /v1/checkout/customers/{id}` \u2014 Update a customer\n\nOnly the fields you send change. Send `null` to clear a field. The wallet address cannot change.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Customer id (uuid). |\n\n**Request body** (`UpdateCustomerRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `email` | string (email) \\| null | No | \u2264 320 chars |\n| `name` | string \\| null | No | \u2264 200 chars |\n| `phone` | string \\| null | No | \u2264 50 chars |\n| `country` | string \\| null | No | \u2264 100 chars |\n| `metadata` | map | No | |\n\n**Response `200`** \u2014 The updated customer.\n\n```\n{\n "data": {\n "id": "string",\n "wallet_address": "string",\n "email": "string",\n "name": "string",\n "phone": "string",\n "country": "string",\n "metadata": {},\n "status": "active",\n "total_spent": "1000000",\n "transaction_count": 0,\n "first_payment_at": "2026-11-06T07:00:00.000Z",\n "last_payment_at": "2026-11-06T07:00:00.000Z",\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n## x402\n\nCharge per API call with HTTP 402 and EIP-3009 authorizations.\n\n### `GET /v1/x402/assets` \u2014 List x402 assets for this mode\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Response `200`** \u2014 Assets.\n\n```\n{\n "data": {\n "mode": "TEST",\n "assets": [\n {\n "caip2": "string",\n "network": "string",\n "chain_name": "string",\n "symbol": "string",\n "decimals": 0,\n "address": "string"\n }\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \u2014 see [Errors](/docs/md/errors.md).\n\n### `POST /v1/x402/requirements` \u2014 Build the 402 payment requirements\n\nReturns the body your resource server sends with `402 Payment Required`, including the fee split. Pass exactly one of `amount` or `amountAtomic`.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`X402RequirementsRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `network` | string | No | x402 network name (`base`, `base-sepolia`). Defaults to the mode\u2019s first asset. (\u2265 1 chars) |\n| `asset` | string | No | Token symbol. Default USDC. (\u2265 1 chars, default `"USDC"`) |\n| `payTo` | string | Yes | Your payout wallet (0x) \u2014 must be the one registered in Console for this mode, otherwise `PAYTO_NOT_MERCHANT_WALLET`. |\n| `amount` | string | No | Gross amount per call as a decimal string, e.g. "0.1". The fee is taken from it. |\n| `amountAtomic` | string | No | Same as `amount`, as a 6-decimal integer string. |\n| `resource` | string | Yes | The URL being paid for. (1\u20132048 chars) |\n| `description` | string | No | \u2264 512 chars |\n| `mimeType` | string | No | \u2264 128 chars |\n| `maxTimeoutSeconds` | integer | No | How long the payer has to sign (max 3600). Default 300. (\u2264 3600, > 0) |\n\n```\n{\n "payTo": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",\n "amount": "0.1",\n "resource": "https://api.example.com/report"\n}\n```\n\n**Response `200`** \u2014 402 body.\n\n```\n{\n "data": {\n "x402Version": 1,\n "accepts": [\n {\n "scheme": "exact",\n "network": "string",\n "asset": "string",\n "payTo": "string",\n "maxAmountRequired": "1000000",\n "resource": "string",\n "description": "string",\n "mimeType": "string",\n "maxTimeoutSeconds": 0,\n "extra": {\n "name": "string",\n "version": "string",\n "chainId": 0\n },\n "feeBreakdown": {\n "netAmount": "1000000",\n "feeAmount": "1000000",\n "feeBps": 0,\n "feeRecipient": "string"\n }\n }\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \u2014 see [Errors](/docs/md/errors.md).\n\n### `POST /v1/x402/verify` \u2014 Verify an X-PAYMENT header\n\nChecks the EIP-3009 authorization against the requirements. Sends nothing on-chain.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`X402PaymentRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `x402Version` | 1 | Yes | |\n| `paymentPayload` | object | Yes | |\n| `paymentRequirements` | object | Yes | |\n\n**Response `200`** \u2014 Valid.\n\n```\n{\n "data": {\n "valid": true,\n "payment_id": "string",\n "status": "string",\n "idempotent": true\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `409` \u2014 see [Errors](/docs/md/errors.md).\n\n### `POST /v1/x402/settle` \u2014 Settle an x402 payment\n\nBroadcasts the transfer and waits for the receipt. Serve the resource only when `status` is `confirmed`. Settlement is idempotent per authorization \u2014 retry with the same body on 5xx.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`X402PaymentRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `x402Version` | 1 | Yes | |\n| `paymentPayload` | object | Yes | |\n| `paymentRequirements` | object | Yes | |\n\n**Response `200`** \u2014 Settlement result.\n\n```\n{\n "data": {\n "success": true,\n "payment_id": "string",\n "status": "string",\n "tx_hash": "string",\n "fee_tx_hash": "string",\n "network": "string",\n "idempotent": true\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `409` \xB7 `503` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/x402/payments/{id}` \u2014 Retrieve an x402 payment\n\nRecovery path when a settle response was lost \u2014 check here before settling again.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Payment id. |\n\n**Response `200`** \u2014 The payment.\n\n```\n{\n "data": {\n "id": "string",\n "status": "pending",\n "raw_status": "string",\n "resource": "string",\n "chain": "string",\n "asset": "string",\n "decimals": 0,\n "amount": "1000000",\n "gross_amount": "1000000",\n "payer": "string",\n "pay_to": "string",\n "tx_hash": "string",\n "created_at": "2026-11-06T07:00:00.000Z",\n "settled_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/x402/payments` \u2014 List x402 payments\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `limit` | query | integer | No | \u2264 100, > 0, default `20` |\n| `offset` | query | integer | No | \u2265 0, default `0` |\n| `status` | query | string | No | pending \\| confirmed \\| failed \\| needs_review |\n\n**Response `200`** \u2014 Payments.\n\n```\n{\n "data": [\n {\n "id": "string",\n "status": "pending",\n "raw_status": "string",\n "resource": "string",\n "chain": "string",\n "asset": "string",\n "decimals": 0,\n "amount": "1000000",\n "gross_amount": "1000000",\n "payer": "string",\n "pay_to": "string",\n "tx_hash": "string",\n "created_at": "2026-11-06T07:00:00.000Z",\n "settled_at": "2026-11-06T07:00:00.000Z"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n## Reports\n\nReconciliation: payments, payouts and fees for a date range, as JSON pages or one CSV.\n\n### `GET /v1/checkout/reports/payments` \u2014 Payments report\n\nRows: every session **created** in the range, in any status, newest first. Only the API key\u2019s mode (test or live) is included. With `format=json` (default) you get one page (`limit`/`offset`), `meta.total` and a `summary` over the whole range. With `format=csv` you get the whole range as one RFC 4180 CSV file (`text/csv`, CRLF line endings; `limit`/`offset` are ignored) with the same columns in the same order \u2014 empty cells for null, `metadata` as a JSON string. Amounts are atomic integer strings: `amount_total`, `fee_amount` and `net_amount` are 6-decimal; `fee_amount_token` and `net_amount_token` are in the payment token\u2019s decimals (`token_decimals`) and match the on-chain transfers exactly.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `from` | query | string (date) | No | First day to include, UTC date `YYYY-MM-DD`. Omit for no lower bound. |\n| `to` | query | string (date) | No | Last day to include (the whole day), UTC date `YYYY-MM-DD`. Omit for no upper bound. Must not be before `from`. |\n| `format` | query | "json" \\| "csv" | No | `json` (default) or `csv`. (default `"json"`) |\n| `limit` | query | integer | No | JSON page size, 1\u2013100 (default 50). Ignored for CSV. (1\u2013100, default `50`) |\n| `offset` | query | integer | No | JSON page offset (default 0). Ignored for CSV. (\u2265 0, default `0`) |\n\n**Response `200`** \u2014 One page of rows. `meta` carries total/limit/offset; `summary` totals the whole range. With `format=csv` the body is a CSV file instead.\n\n```\n{\n "data": [\n {\n "session_id": "string",\n "created_at": "2026-11-06T07:00:00.000Z",\n "paid_at": "2026-11-06T07:00:00.000Z",\n "settled_at": "2026-11-06T07:00:00.000Z",\n "status": "string",\n "currency": "string",\n "amount_total": "1000000",\n "amount_received": "string",\n "pay_chain": "string",\n "pay_asset": "string",\n "payer_wallet": "string",\n "deposit_tx_hash": "string",\n "sweep_tx_hash": "string",\n "fee_rate": "string",\n "fee_amount": "string",\n "net_amount": "string",\n "token_decimals": 0,\n "fee_amount_token": "string",\n "net_amount_token": "string",\n "metadata": {}\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n },\n "summary": {\n "total_count": 0,\n "total_amount_atomic": "1000000",\n "total_fees_atomic": "1000000",\n "total_net_atomic": "1000000"\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/reports/payouts` \u2014 Payouts report\n\nRows: every session **settled** to your payout wallet in the range (by `settled_at`), newest first. Only the API key\u2019s mode (test or live) is included. With `format=json` (default) you get one page (`limit`/`offset`), `meta.total` and a `summary` over the whole range. With `format=csv` you get the whole range as one RFC 4180 CSV file (`text/csv`, CRLF line endings; `limit`/`offset` are ignored) with the same columns in the same order \u2014 empty cells for null, `metadata` as a JSON string. Amounts are atomic integer strings: `amount_total`, `fee_amount` and `net_amount` are 6-decimal; `fee_amount_token` and `net_amount_token` are in the payment token\u2019s decimals (`token_decimals`) and match the on-chain transfers exactly.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `from` | query | string (date) | No | First day to include, UTC date `YYYY-MM-DD`. Omit for no lower bound. |\n| `to` | query | string (date) | No | Last day to include (the whole day), UTC date `YYYY-MM-DD`. Omit for no upper bound. Must not be before `from`. |\n| `format` | query | "json" \\| "csv" | No | `json` (default) or `csv`. (default `"json"`) |\n| `limit` | query | integer | No | JSON page size, 1\u2013100 (default 50). Ignored for CSV. (1\u2013100, default `50`) |\n| `offset` | query | integer | No | JSON page offset (default 0). Ignored for CSV. (\u2265 0, default `0`) |\n\n**Response `200`** \u2014 One page of rows. `meta` carries total/limit/offset; `summary` totals the whole range. With `format=csv` the body is a CSV file instead.\n\n```\n{\n "data": [\n {\n "session_id": "string",\n "settled_at": "2026-11-06T07:00:00.000Z",\n "paid_at": "2026-11-06T07:00:00.000Z",\n "currency": "string",\n "amount_total": "1000000",\n "pay_chain": "string",\n "pay_asset": "string",\n "deposit_tx_hash": "string",\n "sweep_tx_hash": "string",\n "fee_amount": "string",\n "net_amount": "string",\n "token_decimals": 0,\n "fee_amount_token": "string",\n "net_amount_token": "string",\n "metadata": {}\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n },\n "summary": {\n "total_count": 0,\n "total_amount_atomic": "1000000",\n "total_fees_atomic": "1000000",\n "total_net_atomic": "1000000"\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/reports/fees` \u2014 Fees report\n\nRows: every session **paid** in the range (by `paid_at`), with the platform fee, newest first. Only the API key\u2019s mode (test or live) is included. With `format=json` (default) you get one page (`limit`/`offset`), `meta.total` and a `summary` over the whole range. With `format=csv` you get the whole range as one RFC 4180 CSV file (`text/csv`, CRLF line endings; `limit`/`offset` are ignored) with the same columns in the same order \u2014 empty cells for null, `metadata` as a JSON string. Amounts are atomic integer strings: `amount_total`, `fee_amount` and `net_amount` are 6-decimal; `fee_amount_token` and `net_amount_token` are in the payment token\u2019s decimals (`token_decimals`) and match the on-chain transfers exactly.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `from` | query | string (date) | No | First day to include, UTC date `YYYY-MM-DD`. Omit for no lower bound. |\n| `to` | query | string (date) | No | Last day to include (the whole day), UTC date `YYYY-MM-DD`. Omit for no upper bound. Must not be before `from`. |\n| `format` | query | "json" \\| "csv" | No | `json` (default) or `csv`. (default `"json"`) |\n| `limit` | query | integer | No | JSON page size, 1\u2013100 (default 50). Ignored for CSV. (1\u2013100, default `50`) |\n| `offset` | query | integer | No | JSON page offset (default 0). Ignored for CSV. (\u2265 0, default `0`) |\n\n**Response `200`** \u2014 One page of rows. `meta` carries total/limit/offset; `summary` totals the whole range. With `format=csv` the body is a CSV file instead.\n\n```\n{\n "data": [\n {\n "session_id": "string",\n "paid_at": "2026-11-06T07:00:00.000Z",\n "currency": "string",\n "amount_total": "1000000",\n "pay_chain": "string",\n "pay_asset": "string",\n "fee_rate": "string",\n "fee_amount": "string",\n "net_amount": "string",\n "token_decimals": 0,\n "fee_amount_token": "string",\n "net_amount_token": "string",\n "metadata": {}\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n },\n "summary": {\n "total_count": 0,\n "total_amount_atomic": "1000000",\n "total_fees_atomic": "1000000",\n "total_net_atomic": "1000000"\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n## Events\n\nThe checkout events BitPal emits, with their webhook deliveries \u2014 for reconciliation and resending.\n\n### `GET /v1/checkout/events` \u2014 List events\n\nCheckout events of the API key\u2019s environment (test or live), newest first \u2014 the same events your webhook endpoints receive, whether or not you have an endpoint. Use it to reconcile missed webhooks. `meta` carries total/limit/offset.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `page` | query | integer | No | Page number, from 1 (default 1). (\u2265 1, default `1`) |\n| `limit` | query | integer | No | Page size, 1\u2013100 (default 50). (1\u2013100, default `50`) |\n| `event_type` | query | string | No | Only this event, e.g. `checkout.session.paid`. (\u2265 1 chars) |\n| `from` | query | string (date-time) | No | Only events at or after this ISO-8601 time (with offset). |\n| `to` | query | string (date-time) | No | Only events at or before this ISO-8601 time (with offset). |\n\n**Response `200`** \u2014 One page of events.\n\n```\n{\n "data": [\n {\n "id": "00000000-0000-4000-8000-000000000000",\n "event_type": "string",\n "aggregate_type": "string",\n "aggregate_id": "string",\n "payload": {},\n "is_test": true,\n "created_at": "2026-11-06T07:00:00.000Z"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/checkout/events/{id}` \u2014 Retrieve an event with its deliveries\n\nThe event and every webhook delivery made for it (status, attempts, last HTTP status).\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Event id (uuid) \u2014 the envelope `id`. |\n\n**Response `200`** \u2014 The event and its deliveries.\n\n```\n{\n "data": {\n "event": {\n "id": "00000000-0000-4000-8000-000000000000",\n "event_type": "string",\n "aggregate_type": "string",\n "aggregate_id": "string",\n "payload": {},\n "is_test": true,\n "created_at": "2026-11-06T07:00:00.000Z"\n },\n "deliveries": [\n {\n "id": "00000000-0000-4000-8000-000000000000",\n "webhook_id": "00000000-0000-4000-8000-000000000000",\n "event_id": "00000000-0000-4000-8000-000000000000",\n "event_type": "string",\n "status": "string",\n "attempt_count": 0,\n "last_delivery_status": 0,\n "next_retry_at": "2026-11-06T07:00:00.000Z",\n "replayed_from_event_id": "00000000-0000-4000-8000-000000000000",\n "created_at": "2026-11-06T07:00:00.000Z"\n }\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `POST /v1/checkout/events/{id}/retry` \u2014 Resend an event to subscribed endpoints\n\nQueues the event again for every active endpoint in this environment that subscribes to it \u2014 or only `webhook_id`. The envelope keeps the original `id` and `created_at`, so your dedup still applies. To resend to one endpoint, including one created after the event, you can also use `POST /v1/webhooks/{id}/replay`.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Event id (uuid) \u2014 the envelope `id`. |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`RetryCheckoutEventRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `webhook_id` | string (uuid) | No | Optional: resend only to this endpoint (it must be active and subscribed to the event). |\n\n```\n{}\n```\n\n**Response `200`** \u2014 The queued deliveries.\n\n```\n{\n "data": {\n "message": "string",\n "delivery_ids": [\n "00000000-0000-4000-8000-000000000000"\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n## Sandbox\n\nSimulate a buyer payment in test mode and follow it, without a wallet; inspect webhook events and their deliveries.\n\n### `POST /v1/sandbox/sessions/{id}/simulate` \u2014 Simulate a buyer payment\n\nTest mode only: BitPal sends a testnet deposit to the session for you, so you can test fulfilment and webhooks without a wallet. Scenarios: `exact` (pays the rest \u2014 the session gets paid), `underpay` (half of the rest), `overpay` (the rest + 1 token \u2014 the excess is refunded), `wrong_token` (1 unit of another token on the same chain \u2014 returned; the session stays payable). Issues the deposit address first if needed (a scenario the chain cannot run is rejected before an address is issued). `wrong_token` needs a second test token on the chain \u2014 today BNB Smart Chain Testnet and Polygon Amoy; elsewhere 400 `SANDBOX_SCENARIO_UNAVAILABLE` lists the chains that work (`details.supported_chains`). **Late payments:** on a session whose deposit address was issued and that then expired unpaid (`expires_at` + 90 min), `exact`/`overpay`/`underpay` send a payment after the deadline \u2014 `late_payments: "accept"` \u2192 `checkout.session.paid` with `late: true`; `"refund"` \u2192 `checkout.session.unresolved` with `reason: "late"` and an automatic refund about 7 days after the deadline. A session that expired without an address cannot be paid (409 `SANDBOX_SESSION_NOT_PAYABLE`). A `bp_live_` key gets 403 `SANDBOX_TEST_MODE_ONLY`. Limited to 10 calls per minute and 300 per day per account.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Checkout session id (`cs_\u2026`). |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`SimulateSandboxPaymentRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `scenario` | "exact" \\| "underpay" \\| "overpay" \\| "wrong_token" | Yes | `exact`, `underpay`, `overpay` or `wrong_token`. |\n| `chain` | string | No | Only before the deposit address is issued, for a session with several payment options: the network to pay on (with `token`). (\u2265 1 chars) |\n| `token` | string | No | Goes with `chain`, e.g. `USDC`. (\u2265 1 chars) |\n| `refund_address` | string | No | Buyer refund address, used only when this call issues the deposit address. Omit it and refunds go to BitPal\u2019s test buyer wallet. (1\u2013128 chars) |\n\n```\n{\n "scenario": "exact"\n}\n```\n\n**Response `201`** \u2014 The deposit was sent.\n\n```\n{\n "data": {\n "session_id": "string",\n "scenario": "exact",\n "chain": "string",\n "token": "string",\n "amount": "1000000",\n "decimals": 0,\n "deposit_address": "string",\n "tx_hash": "string"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `404` \xB7 `409` \xB7 `429` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/sandbox/sessions/{id}/progress` \u2014 Follow a test payment\n\nTest mode only. One view of a test session: stage, confirmations while the deposit confirms, deposit and settlement transactions with explorer links, and refunds. Calling it also checks for the deposit right away. Stop polling once `terminal` is true.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Checkout session id (`cs_\u2026`). |\n\n**Response `200`** \u2014 The session\u2019s progress.\n\n```\n{\n "data": {\n "session_id": "string",\n "status": "string",\n "stage": "awaiting_address",\n "terminal": true,\n "chain": "string",\n "token": "string",\n "token_decimals": 0,\n "amount_received": "string",\n "deposit_address": "string",\n "deposit_tx_hash": "string",\n "deposit_tx_url": "https://example.com",\n "sweep_tx_hash": "string",\n "sweep_tx_url": "https://example.com",\n "confirmations": {\n "current": 0,\n "required": 1\n },\n "created_at": "string",\n "paid_at": "string",\n "settled_at": "string",\n "expires_at": "string",\n "refunds": [\n {\n "kind": "string",\n "amount": "1000000",\n "amount_display": "string",\n "token_symbol": "string",\n "tx_hash": "string",\n "tx_url": "https://example.com",\n "created_at": "string"\n }\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/sandbox/events` \u2014 List webhook events with delivery results\n\nWebhook events of the API key\u2019s environment (test or live), newest first, each as the exact body your endpoint receives plus the result per endpoint. Works without any endpoint registered. Filter by `session_id` to follow one test payment.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `session_id` | query | string | No | Only events of this session (`cs_\u2026`). |\n| `limit` | query | integer | No | Max events, 1\u2013100 (default 50). (1\u2013100, default `50`) |\n\n**Response `200`** \u2014 Events with their deliveries.\n\n```\n{\n "data": {\n "events": [\n {\n "envelope": {\n "id": "string",\n "event": "string",\n "created_at": "string",\n "data": {}\n },\n "is_test": true,\n "deliveries": [\n {\n "webhook_id": "00000000-0000-4000-8000-000000000000",\n "endpoint_url": "string",\n "status": "string",\n "attempt_count": 0,\n "last_http_status": 0\n }\n ]\n }\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n## Webhook endpoints\n\nManage where BitPal delivers signed events.\n\n### `POST /v1/webhooks` \u2014 Create a webhook endpoint\n\n**Test API key or console.** A `bp_test_` key can call this for test mode; a `bp_live_` key gets 403 \u2014 manage live endpoints in Console \u2192 Developers \u2192 Webhooks. The response carries the `whsec_\u2026` signing secret \u2014 it is shown only once. With a `bp_test_` key this creates a test-mode endpoint (for CI, or local development behind a tunnel); live endpoints are created in the console. Recorded in your audit log.\n\n**Auth:** Test API key (`bp_test_\u2026`) or console login \u2014 live keys get 403; manage live endpoints in Console \u2192 Developers \u2192 Webhooks\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`CreateWebhookEndpointRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `endpoint_url` | string (uri) | Yes | HTTPS URL that receives events. Private hosts are rejected. |\n| `subscribed_events` | array of ("*" \\| "checkout.session.created" \\| "checkout.session.deposit_detected" \\| "checkout.session.paid" \\| "checkout.session.settled" \\| "checkout.session.expired" \\| "checkout.session.unresolved" \\| "checkout.refund.recorded" \\| "x402.payment.resolved") | Yes | Event names to deliver (see the `webhooks` section), or `*` for every event. Unknown names are rejected with 400. (\u2265 1 items) |\n| `retry_policy` | object | No | `max_retries` caps the total delivery attempts (1\u201320, default 10: 3 right away, then 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h). The schedule itself is fixed; `backoff` is ignored. |\n| `dead_letter_destination` | string (uri) \\| null | No | Optional URL for dead-lettered deliveries. |\n\n```\n{\n "endpoint_url": "https://your.store/webhooks/bitpal",\n "subscribed_events": [\n "checkout.session.paid"\n ]\n}\n```\n\n**Response `201`** \u2014 Endpoint created.\n\n```\n{\n "data": {\n "id": "00000000-0000-4000-8000-000000000000",\n "mode": "TEST",\n "endpoint_url": "string",\n "subscribed_events": [\n "string"\n ],\n "retry_policy": {\n "max_retries": 0,\n "backoff": "exponential"\n },\n "dead_letter_destination": "string",\n "status": "active",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z",\n "secret": "string"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).\n\n### `GET /v1/webhooks` \u2014 List webhook endpoints\n\n**Test API key or console.** A `bp_test_` key can call this for test mode; a `bp_live_` key gets 403 \u2014 manage live endpoints in Console \u2192 Developers \u2192 Webhooks. Endpoints of the caller\u2019s environment and mode (a `bp_test_` key lists test-mode endpoints).\n\n**Auth:** Test API key (`bp_test_\u2026`) or console login \u2014 live keys get 403; manage live endpoints in Console \u2192 Developers \u2192 Webhooks\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `limit` | query | integer | No | 1\u2013100, default `50` |\n| `offset` | query | integer | No | \u2265 0, default `0` |\n\n**Response `200`** \u2014 Endpoints, newest first. `meta` carries total/limit/offset.\n\n```\n{\n "data": [\n {\n "id": "00000000-0000-4000-8000-000000000000",\n "mode": "TEST",\n "endpoint_url": "string",\n "subscribed_events": [\n "string"\n ],\n "retry_policy": {\n "max_retries": 0,\n "backoff": "exponential"\n },\n "dead_letter_destination": "string",\n "status": "active",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \u2014 see [Errors](/docs/md/errors.md).\n\n### `PATCH /v1/webhooks/{id}` \u2014 Update a webhook endpoint\n\n**Test API key or console.** A `bp_test_` key can call this for test mode; a `bp_live_` key gets 403 \u2014 manage live endpoints in Console \u2192 Developers \u2192 Webhooks. Setting `status` to `disabled` removes the endpoint. In the console a `reason_code` is required (otherwise 400 `REASON_CODE_REQUIRED`); with a `bp_test_` key it is optional and a request without it is recorded as `api_request`. Recorded in the audit log.\n\n**Auth:** Test API key (`bp_test_\u2026`) or console login \u2014 live keys get 403; manage live endpoints in Console \u2192 Developers \u2192 Webhooks\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Webhook endpoint id (uuid). |\n\n**Request body** (`UpdateWebhookEndpointRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `endpoint_url` | string (uri) | No | |\n| `subscribed_events` | array of ("*" \\| "checkout.session.created" \\| "checkout.session.deposit_detected" \\| "checkout.session.paid" \\| "checkout.session.settled" \\| "checkout.session.expired" \\| "checkout.session.unresolved" \\| "checkout.refund.recorded" \\| "x402.payment.resolved") | No | \u2265 1 items |\n| `status` | "active" \\| "paused" \\| "disabled" | No | |\n| `retry_policy` | object | No | |\n| `dead_letter_destination` | string (uri) \\| null | No | |\n| `reason_code` | "user_request" \\| "security_concern" \\| "rotation" \\| "no_longer_needed" \\| "other" | No | When `status` is `disabled`: one of user_request, security_concern, rotation, no_longer_needed, other. Required in the console; optional with a test API key (recorded as `api_request`). |\n| `reason_note` | string | No | Optional free text (\u2264 500 chars) stored with the audit record. (\u2264 500 chars) |\n\n**Response `200`** \u2014 The updated endpoint.\n\n```\n{\n "data": {\n "id": "00000000-0000-4000-8000-000000000000",\n "mode": "TEST",\n "endpoint_url": "string",\n "subscribed_events": [\n "string"\n ],\n "retry_policy": {\n "max_retries": 0,\n "backoff": "exponential"\n },\n "dead_letter_destination": "string",\n "status": "active",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `POST /v1/webhooks/{id}/rotate-secret` \u2014 Rotate the signing secret\n\n**Test API key or console.** A `bp_test_` key can call this for test mode; a `bp_live_` key gets 403 \u2014 manage live endpoints in Console \u2192 Developers \u2192 Webhooks. Takes effect immediately: the old secret stops working and the new one is returned once \u2014 update your verifier. In the console a `reason_code` is required (400 `REASON_CODE_REQUIRED` if missing); with a `bp_test_` key it is optional and recorded as `api_request`. Recorded in the audit log.\n\n**Auth:** Test API key (`bp_test_\u2026`) or console login \u2014 live keys get 403; manage live endpoints in Console \u2192 Developers \u2192 Webhooks\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Webhook endpoint id (uuid). |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`RotateWebhookSecretRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `reason_code` | "user_request" \\| "security_concern" \\| "rotation" \\| "no_longer_needed" \\| "other" | No | One of user_request, security_concern, rotation, no_longer_needed, other. Required in the console; optional with a test API key. |\n| `reason_note` | string | No | Optional free text (\u2264 500 chars) stored with the audit record. (\u2264 500 chars) |\n\n```\n{\n "reason_code": "rotation"\n}\n```\n\n**Response `200`** \u2014 Endpoint with the new secret.\n\n```\n{\n "data": {\n "id": "00000000-0000-4000-8000-000000000000",\n "mode": "TEST",\n "endpoint_url": "string",\n "subscribed_events": [\n "string"\n ],\n "retry_policy": {\n "max_retries": 0,\n "backoff": "exponential"\n },\n "dead_letter_destination": "string",\n "status": "active",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z",\n "secret": "string"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `POST /v1/webhooks/{id}/replay` \u2014 Resend a past event\n\n**Test API key or console.** A `bp_test_` key can call this for test mode; a `bp_live_` key gets 403 \u2014 manage live endpoints in Console \u2192 Developers \u2192 Webhooks. Queues one past event for delivery to this endpoint \u2014 including events from before the endpoint existed, so a newly registered endpoint can catch up. `event_id` is the envelope `id` of the event (unchanged, so your dedup still applies); the body is the original `data`, signed with the endpoint\u2019s current secret and delivered with the usual retries. Only events of the caller\u2019s environment (test or live) can be resent. Recorded in the audit log.\n\n**Auth:** Test API key (`bp_test_\u2026`) or console login \u2014 live keys get 403; manage live endpoints in Console \u2192 Developers \u2192 Webhooks\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Webhook endpoint id (uuid). |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`ReplayWebhookEventRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `event_id` | string (uuid) | Yes | Event id (the envelope `id`, uuid) \u2014 from an earlier delivery, or from the MCP `list_test_events` tool in test mode. |\n\n```\n{\n "event_id": "1ff63374-9417-4f87-850d-e869a3bd2dbe"\n}\n```\n\n**Response `201`** \u2014 The queued delivery.\n\n```\n{\n "data": {\n "id": "00000000-0000-4000-8000-000000000000",\n "webhook_id": "00000000-0000-4000-8000-000000000000",\n "event_id": "00000000-0000-4000-8000-000000000000",\n "event_type": "string",\n "status": "string",\n "attempt_count": 0,\n "replayed_from_event_id": "00000000-0000-4000-8000-000000000000",\n "created_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).\n\n### `POST /v1/developers/webhooks/test` \u2014 Send a test event\n\nPosts one signed sample event to an endpoint (no retries). The envelope id is prefixed `test_`, `data.test` is true and the request carries `X-Webhook-Test: true`.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`SendTestWebhookRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `webhook_id` | string (uuid) | Yes | Endpoint id. |\n| `event_type` | string | Yes | Event name to put in the envelope, e.g. checkout.session.paid. (1\u2013200 chars) |\n| `payload` | map | No | Custom `data` object. Defaults to a sample message. |\n\n```\n{\n "webhook_id": "00000000-0000-4000-8000-000000000000",\n "event_type": "checkout.session.paid"\n}\n```\n\n**Response `200`** \u2014 Delivery result.\n\n```\n{\n "data": {\n "delivery_id": "00000000-0000-4000-8000-000000000000",\n "webhook_id": "00000000-0000-4000-8000-000000000000",\n "endpoint_url": "string",\n "event_type": "string",\n "http_status": 0,\n "response_body": "string",\n "error": "string",\n "duration_ms": 0,\n "test": true,\n "sent_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `404` \xB7 `409` \u2014 see [Errors](/docs/md/errors.md).\n\n## Webhook events\n\nEnvelope: `{ id, event, created_at, data }`, signed with `X-Webhook-Signature-256`. Verification and retries: [Webhooks](/docs/md/webhooks.md).\n\n| Event | Fires when | `data` fields |\n| --- | --- | --- |\n| `checkout.session.created` | A session is created. | `session_id`, `metadata`, `amount_total`, `currency`, `late` |\n| `checkout.session.deposit_detected` | Funds seen on-chain, awaiting confirmations. Optional \u2014 skipped when the first time BitPal sees the deposit it is already confirmed; then `checkout.session.paid` (or `unresolved`) arrives with no `deposit_detected` before it. Do not wait for it. | `session_id`, `metadata`, `amount_total`, `currency`, `late`, `deposit_tx_hash`, `received_token_symbol`, `token_decimals`, `tx_hash`, `amount_received`, `received_token` |\n| `checkout.session.paid` | Confirmed \u2014 safe to fulfil. `late: true` if it arrived after the deadline. | `session_id`, `metadata`, `amount_total`, `currency`, `late`, `deposit_tx_hash`, `received_token_symbol`, `token_decimals`, `tx_hash` |\n| `checkout.session.settled` | Funds swept to your payout wallet. | `session_id`, `metadata`, `amount_total`, `currency`, `late`, `deposit_tx_hash`, `received_token_symbol`, `token_decimals`, `sweep_tx_hash` |\n| `checkout.session.unresolved` | Needs attention (`reason`): `underpaid`, `late` (a full payment arrived after the deadline on a session created with `late_payments: "refund"` \u2014 it is not accepted and is refunded automatically at `refund_available_at`; sent once), or `reorg` (a confirmed deposit was reverted). `wrong_token` only on sessions created before 2026-11-12. | `session_id`, `metadata`, `amount_total`, `currency`, `late`, `deposit_tx_hash`, `received_token_symbol`, `token_decimals`, `reason`, `amount_expected`, `amount_received`, `received_token`, `tx_hash`, `refund_available_at` |\n| `checkout.session.expired` | Deadline passed without full payment. Timing: a session that never got a deposit address expires at `expires_at` (applied within about 5 minutes, or at once when the session is read). Once a deposit address was issued, this event is sent shortly after the 90-minute grace that follows `expires_at` ends (`expires_at` + 90 min), because a transfer already in flight may still confirm. If you cannot hold stock that long, release it yourself at `expires_at`; a full payment can still arrive later as `checkout.session.paid` with `late: true` (unless the session was created with `late_payments: "refund"`). | `session_id`, `metadata`, `amount_total`, `currency`, `late`, `deposit_tx_hash`, `received_token_symbol`, `token_decimals`, `reason`, `expired_at`, `source`, `refund_tx_hash` |\n| `checkout.refund.recorded` | A refund was made on-chain (`kind`): automatic, or recorded by you in the console (`merchant`). | `session_id`, `metadata`, `kind`, `amount`, `token`, `token_symbol`, `token_decimals`, `tx_hash`, `refund_address`, `from_address`, `reason_code` |\n| `x402.payment.resolved` | An x402 call that settled with `503` reached its outcome. Only fires for those \u2014 the normal path answers inline. | `payment_id`, `status`, `resource`, `amount`, `asset`, `chain`, `tx_hash`, `payer`, `resolved_by` |'
|
|
4071
4071
|
},
|
|
4072
4072
|
{
|
|
4073
4073
|
slug: "errors",
|
|
@@ -4081,14 +4081,14 @@ var docs_data_default = {
|
|
|
4081
4081
|
title: "Popup & headless",
|
|
4082
4082
|
url: "https://bitpal.io/docs/#flows",
|
|
4083
4083
|
markdownUrl: "https://bitpal.io/docs/md/flows.md",
|
|
4084
|
-
markdown: '# Popup & headless\n\nChoose how the buyer sees the payment. Every flow ends the same way: you fulfil from the webhook.\n\n## Choose a flow\n\n| Flow | Buyer experience | You build |\n| --- | --- | --- |\n| **Redirect** | Leaves your site for the hosted page, returns to `success_url`. | Create a session, redirect. Simplest. |\n| **Popup** | Hosted page in a small window; your page stays open. | Open the window, listen for one message. |\n| **Headless** | Your own payment UI: options, address, QR, status. | A few backend endpoints plus your UI. |\n| **Payment link** | A shareable URL; no code. \u2192 [Payment links](/docs/md/links.md) | Nothing. |\n| **x402** | No page \u2014 an API client pays per call. \u2192 [x402](/docs/md/x402.md) | Middleware on the route. |\n\nThe hosted page shows the buyer who they are paying \u2014 your organization name from **Console \u2192 Settings \u2192 Organization** \u2014 and the session\'s line items. Set the organization name before going live; until it is set, only the line items are shown. In a headless UI, read the same values from `getPublicSession(id)` (`merchant_name`, `line_items`).\n\n## Popup\n\nBrowsers block `window.open` unless it runs directly in the click handler. Open an empty window first, then point it at the checkout URL once your backend has created the session.\n\n```\nasync function onPayClick() {\n // 1) open synchronously, inside the click handler\n const popup = window.open(\'\', \'bitpal_checkout\', \'width=460,height=720\');\n\n // 2) your backend creates the session (secret key stays on the server)\n const { checkout_url, session_id } = await fetch(\'/api/checkout\', { method: \'POST\' }).then((r) => r.json());\n\n // 3) navigate the popup \u2014 or fall back to a full redirect if it was blocked\n if (popup && !popup.closed) popup.location.href = checkout_url;\n else window.location.href = checkout_url;\n\n const origin = new URL(checkout_url).origin;\n window.addEventListener(\'message\', function onMessage(e) {\n if (e.origin !== origin) return; // trust only the checkout page\n const m = e.data;\n if (!m || m.source !== \'bitpal-checkout\' || m.type !== \'terminal\') return;\n window.removeEventListener(\'message\', onMessage);\n refreshOrderFromServer(session_id); // m.step: done | expired | error\n });\n}\n```\n\nWhen the payment reaches a final screen, the checkout page posts one message to its opener:\n\n```\n{ "source": "bitpal-checkout", "type": "terminal", "step": "done", "session_id": "cs_\u2026" }\n```\n\n- `step` is `done`, `expired` or `error`. One `terminal` message is sent per final screen.\n- If the buyer sends the wrong token, the page posts `{ "type": "status", "step": "wrong_token" }` instead. It is not final: the wrong token is returned
|
|
4084
|
+
markdown: '# Popup & headless\n\nChoose how the buyer sees the payment. Every flow ends the same way: you fulfil from the webhook.\n\n## Choose a flow\n\n| Flow | Buyer experience | You build |\n| --- | --- | --- |\n| **Redirect** | Leaves your site for the hosted page, returns to `success_url`. | Create a session, redirect. Simplest. |\n| **Popup** | Hosted page in a small window; your page stays open. | Open the window, listen for one message. |\n| **Headless** | Your own payment UI: options, address, QR, status. | A few backend endpoints plus your UI. |\n| **Payment link** | A shareable URL; no code. \u2192 [Payment links](/docs/md/links.md) | Nothing. |\n| **x402** | No page \u2014 an API client pays per call. \u2192 [x402](/docs/md/x402.md) | Middleware on the route. |\n\nThe hosted page shows the buyer who they are paying \u2014 your organization name from **Console \u2192 Settings \u2192 Organization** \u2014 and the session\'s line items. Set the organization name before going live; until it is set, only the line items are shown. In a headless UI, read the same values from `getPublicSession(id)` (`merchant_name`, `line_items`).\n\n## Popup\n\nBrowsers block `window.open` unless it runs directly in the click handler. Open an empty window first, then point it at the checkout URL once your backend has created the session.\n\n```\nasync function onPayClick() {\n // 1) open synchronously, inside the click handler\n const popup = window.open(\'\', \'bitpal_checkout\', \'width=460,height=720\');\n\n // 2) your backend creates the session (secret key stays on the server)\n const { checkout_url, session_id } = await fetch(\'/api/checkout\', { method: \'POST\' }).then((r) => r.json());\n\n // 3) navigate the popup \u2014 or fall back to a full redirect if it was blocked\n if (popup && !popup.closed) popup.location.href = checkout_url;\n else window.location.href = checkout_url;\n\n const origin = new URL(checkout_url).origin;\n window.addEventListener(\'message\', function onMessage(e) {\n if (e.origin !== origin) return; // trust only the checkout page\n const m = e.data;\n if (!m || m.source !== \'bitpal-checkout\' || m.type !== \'terminal\') return;\n window.removeEventListener(\'message\', onMessage);\n refreshOrderFromServer(session_id); // m.step: done | expired | error\n });\n}\n```\n\nWhen the payment reaches a final screen, the checkout page posts one message to its opener:\n\n```\n{ "source": "bitpal-checkout", "type": "terminal", "step": "done", "session_id": "cs_\u2026" }\n```\n\n- `step` is `done`, `expired` or `error`. One `terminal` message is sent per final screen.\n- If the buyer sends the wrong token, the page posts `{ "type": "status", "step": "wrong_token" }` instead. It is not final: the wrong token is returned, the session stays `awaiting_deposit` and can still be paid with the right one, so keep listening for `terminal`.\n- The message is a **UI hint**. Re-read the order from your server (which learned the result from the webhook) before showing success.\n- Messages can be lost (the buyer closes the window, browser privacy settings). Keep a fallback: poll your own backend for the order status.\n- Don\'t close the popup yourself \u2014 the completion screen lets the buyer email a receipt. The popup closes itself only after the buyer sends that receipt; otherwise the buyer closes it with the **Close** button, so don\'t wait for the window to close.\n\n## Headless (your own UI)\n\nUse this when you render the payment screen yourself. The buyer still pays to a per-order deposit address; you just display it.\n\n> **Call BitPal from your backend.** Browsers on your domain may only call the public session endpoint directly (CORS). Issue the deposit address and poll status through your server, and never ship a secret key to the browser.\n\n1. **Create the session** on your server (as in the Quickstart). Keep `id` and `session_token`.\n2. **Show the payment options.** `getPublicSession(id)` returns `payment_options` \u2014 one entry per allowed (chain, token) with `caip2`, `token_symbol`, `decimals` and the exact `amount_atomic`.\n3. **Collect a refund address** from the buyer, in the format of the chosen network (EVM `0x\u2026`, Tron `T\u2026`, Solana base58). It is committed into the deposit address and cannot change.\n4. **Issue the deposit address** (backend):\n\n ```\n const { data } = await bitpal.checkout.issueDepositAddress(session.id, {\n session_token: session.session_token,\n refundAddress: \'0xBuyerRefundWallet\u2026\',\n chain: \'eip155:84532\',\n token: \'USDC\',\n });\n // {\n // "sessionId": "cs_\u2026",\n // "chain": "eip155:84532",\n // "depositAddress": "0x6b19e871c934aB52775B6712e110f3e4b0626347",\n // "expectedToken": "0x943b94dacf1507f2658f1cfe103ac36ebc13ef65",\n // "expectedAmount": "29000000", // in the token\'s own decimals\n // "feeAmount": "435000",\n // "refundAddress": "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720",\n // "deadline": "1793957308", // on-chain deadline (unix s) = expiresAt + 5400 (90-minute grace)\n // "expiresAt": "2026-11-06T07:58:28.776Z"\n // }\n ```\n5. **Display** the address, a QR code and the exact amount \u2014 format `expectedAmount` with the token\'s decimals (6 for most USDC/USDT, 18 for BNB Chain USDT/USDC and JPYC). Show the countdown to `expiresAt`.\n6. **Track status** through your backend (`getStatus(id)`) for the UI, and **fulfil from the webhook**.\n\nCalling `issueDepositAddress` again for the same session returns the same address **and keeps the first refund address** \u2014 a later call cannot change it. A deposit address belongs to one order; don\'t reuse it.\n\n> **Tell the buyer to send only the selected token, on the selected network.** BitPal returns automatically only the stablecoins it supports on that network. Any other token is not returned automatically, and the network\'s **native coin** (ETH, BNB, POL, TRX, SOL \u2026) sent to a deposit address **cannot be recovered by BitPal**. Show this next to the address.'
|
|
4085
4085
|
},
|
|
4086
4086
|
{
|
|
4087
4087
|
slug: "lifecycle",
|
|
4088
4088
|
title: "Payment lifecycle",
|
|
4089
4089
|
url: "https://bitpal.io/docs/#lifecycle",
|
|
4090
4090
|
markdownUrl: "https://bitpal.io/docs/md/lifecycle.md",
|
|
4091
|
-
markdown: "# Payment lifecycle\n\nWhat a session goes through, what each state means for you, and how amounts, late payments and refunds work.\n\n## States\n\n```\ncreated \u2500\u2500\u25B6 awaiting_deposit \u2500\u2500\u25B6 deposit_detected \u2500\u2500\u25B6 paid_confirmed \u2500\u2500\u25B6 swept\n \u2502 \u25B2 \u2502 \u2502\n \u2502 \u2502 \u2514\u2500\u25B6 unresolved_underpaid \u2500\u2500(deadline)\u2500\u2500\u25B6 expired (partial refund)\n \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500 wrong_token (wrong asset returned; still payable)\n \u2514\u2500\u2500(deadline, nothing paid)\u2500\u2500\u25B6 expired\n```\n\n| Status | Meaning | What you do |\n| --- | --- | --- |\n| `created` | Session exists; the buyer has not picked a payment option yet. | Nothing. |\n| `awaiting_deposit` | A deposit address was issued; waiting for funds. | Nothing. |\n| `deposit_detected` | Funds seen on-chain, waiting for confirmations. | Show \"processing\". **Don't fulfil yet.** |\n| `paid_confirmed` | The full amount is confirmed. | **Fulfil** (webhook `checkout.session.paid`). |\n| `swept` | Funds moved to your payout wallet, minus the fee. | Reconcile (webhook `checkout.session.settled`). |\n| `unresolved_underpaid` | Less than the amount arrived. The buyer can top up until the deadline. | Wait. If it is still short at the deadline, it is refunded and the session expires. |\n| `wrong_token` | Another stablecoin that BitPal supports on that network was sent (e.g. USDT instead of USDC). It is returned to the refund address; the session stays payable. Other tokens are not detected (see Refunds). | Nothing. |\n| `expired` | The payment window closed without a full payment. | Release the order. A full payment can still arrive later \u2014 see Late payments. |\n\n> **Chain reorganisations.** Rarely, a confirmed deposit disappears after a chain reorg. You then receive `checkout.session.unresolved` with `reason: \"reorg\"` and the session goes back to `awaiting_deposit` (or `expired` past the deadline). If you have already fulfilled, treat it like a reversed payment.\n\n## Amounts\n\n| Field | Unit |\n| --- | --- |\n| `line_items[].amount` (input) | Plain number string in the line item's `currency`: `\"29\"`, `\"0.5\"`. `USDC`/`USDT` = USD, `JPYC` = JPY. No FX conversion. |\n| `amount_atomic` (input), `amount_total` | Always a **6-decimal integer string**, whatever token the buyer pays with: `\"29000000\"` = 29. |\n| `expectedAmount`, `pay_amount`, `amount_received`, webhook `amount_expected`/`amount_received`, refund `amount` | Integer string in the **pay token's own decimals** (`token_decimals`: 6 for USDC/USDT on most chains, 18 for BNB USDT and JPYC). |\n| `fee_amount`, `net_amount` | 6-decimal integer strings; `fee_rate` is a decimal fraction (`\"0.015\"` = 1.5%). |\n\nHandle all amounts as strings or big integers \u2014 never as floating-point numbers.\n\n**Minimum amount.** A session's total (sum of `amount \xD7 quantity`) must be at least **1.00 in the session currency** (USDC/USDT: $1, JPYC: \xA51) \u2014 a lower total returns `400 CHECKOUT_AMOUNT_TOO_LOW`. The schema can't express this (it is a rule on the sum, and `amount` alone may be `\"0\"`), so check it before calling. Per-network minimums come on top: Tron currently $300 (testnet included) and JPYC \xA5100 on every network. A network whose minimum is above the total is not offered to the buyer; requesting a deposit address on it returns `400 CHECKOUT_BELOW_MIN_PAYMENT`.\n\n## Session fields worth knowing\n\n- `deposit_tx_hash` is the buyer's payment into the deposit address; `sweep_tx_hash` is the transaction that settled it to your payout wallet (or, for a refunded or rescued session, that on-chain action).\n- `tx_hash` is **deprecated**. It belonged to an earlier payment path and is kept only for compatibility \u2014 it now returns the same value as `deposit_tx_hash`. Use `deposit_tx_hash` / `sweep_tx_hash`.\n- `received_token` is the **address** of the token contract (or Solana mint) that actually arrived \u2014 not a symbol. `received_token_symbol` gives the symbol (`USDC`, `USDT` \u2026), or `null` for a token BitPal doesn't know (for example a wrong token).\n- `payer_wallet` is the address the deposit came from (or the wallet the buyer connected), in the network's own format \u2014 Tron addresses are base58 `T\u2026`. It may be an exchange's hot wallet, so don't use it as the buyer's identity or refund destination; refunds go to `refund_address`.\n- `metadata` is returned on the session as you sent it, and is also included in every webhook's `data` (see Webhooks \u2192 Event data). Keys BitPal writes itself \u2014 `_bitpal`, `payer_email`, `allowed_pay_chains`, `allowed_assets` \u2014 are reserved and are left out of both; don't use them for your own data.\n\n## Buyer experience\n\n- **Receipts.** BitPal does not email the buyer automatically. After paying, the buyer can request an emailed receipt on the payment completion screen; `customer_email` only pre-fills that form. If every buyer needs a receipt or order confirmation, send it yourself from your `checkout.session.paid` handler.\n- **Status link.** `checkout_url` (`/pay/{id}?token=\u2026`) is also the buyer's status page \u2014 opening it later shows the payment's current state, and the receipt email links back to it. The `token` grants access to that page, so share the URL only with the buyer.\n- **Language.** The payment page picks its language from the buyer's browser (12 languages, English as fallback), and the buyer can switch it. You can't set the language from the session or the URL yet.\n- **Your name and logo.** The payment page does not show a merchant name or logo yet. Buyers see the line item `name`s and amounts \u2014 make item names recognisable (for example include your store name).\n- **Support.** BitPal doesn't contact your buyers. Product, order and refund questions are yours to answer (completed payments are refunded by you \u2014 see Refunds above). For payment problems \u2014 funds sent but not detected, a wrong network, a late transfer \u2014 contact BitPal support with the session id and the transaction hash.\n\n## Payment window and late payments\n\n- The buyer has `expires_in_minutes` (1\u201360, default 15) from session creation to pay. That deadline is the session's `expires_at`.\n- `late` compares the **block time of the deposit transaction** with `expires_at`. A transfer sent in time is `\"late\": false` even if its confirmations finish after the deadline; `checkout.session.settled` carries the same value as `paid`.\n- Once a deposit address has been issued, BitPal keeps the session open for **90 minutes after `expires_at`** so transfers already in flight can confirm. Underpayment refunds and `checkout.session.expired` for an issued session happen at the end of that window \u2014 so for such a session the `expired` event arrives about 90 minutes after `expires_at`, not at `expires_at`.\n- A session that never got a deposit address (the buyer never picked a network) expires at `expires_at`: BitPal marks it expired within about 5 minutes, or immediately when the session is read, and sends `checkout.session.expired` with `reason: \"no_deposit\"` then.\n- **Holding stock?** If you cannot keep an item reserved for the extra 90 minutes, release it yourself at `expires_at` instead of waiting for the event. A full payment that still arrives is then reported as `checkout.session.paid` with `\"late\": true` (or refunded, with `\"late_payments\": \"refund\"`), so be ready to fulfil or refund it.\n- **A full payment that arrives after the deadline is accepted as a late payment** for up to 7 days by default: you get `checkout.session.paid` with `\"late\": true` and the funds settle normally. Decide whether to fulfil, or refund it from the console (see Refunds below).\n- **Don't want late payments?** Create the session with `\"late_payments\": \"refund\"`. A full payment that arrives after the deadline (`expires_at` + 90 minutes) is then never accepted: it stays in the deposit address and is refunded automatically to the buyer's refund address **about 7 days after the deadline** (refund kind `late_deposit`, `checkout.refund.recorded`). The refund can't be instant \u2014 BitPal is non-custodial and the deposit contract only lets anyone but you take the funds back after that 7-day window. A transfer sent before `expires_at` whose confirmations finish later is still accepted.\n- This only applies to a session with no earlier on-chain action. If a wrong token was already returned or an underpayment already refunded, the expired session is closed for good: later deposits to its address are not accepted as payment (contact support with the session id and transaction hash).\n- Acceptance ends 1 hour before the on-chain refund window opens (`expires_at` + 90 minutes + 7 days). A full payment that arrives after that cutoff is not accepted; it is refunded automatically once the window opens (refund kind `late_deposit`).\n- A **partial** payment after the deadline is refunded automatically (refund kind `late_deposit`).\n- Funds sent more than 7 days after the deadline are not watched automatically. Contact support with the session id and transaction hash.\n\n## Refunds\n\n**BitPal is non-custodial.** A payment goes from the buyer to a one-time deposit address and then straight to your payout wallet \u2014 BitPal never holds it and has no right to pull it back. So there is **no refund API**. There are two kinds of refund, and both fire `checkout.refund.recorded`:\n\n### 1. Automatic refunds (mismatched payments)\n\nPayments that don't match the order are returned on-chain from the deposit address to the buyer's refund address \u2014 automatically, with no action from you.\n\n| `kind` | When | Session outcome |\n| --- | --- | --- |\n| `overpay_excess` | More than the amount arrived. | Paid; the exact amount settles, the excess is refunded. |\n| `underpay_expired` | Still short at the deadline. | Expired; everything received is refunded. |\n| `wrong_token` | Another stablecoin that BitPal supports on that network was sent instead of the selected one. | Unchanged; still payable with the right token. |\n| `late_deposit` | A partial payment arrived after the deadline, or a full payment arrived after the late-acceptance cutoff (see above). | Stays expired. |\n\n- **Only supported stablecoins are returned automatically** \u2014 the tokens BitPal supports on that network (see [Chains & contracts](/docs/md/reference.md)). Any other token is not watched: it is not returned automatically and the session does not change. On EVM networks and Tron it can be returned to the buyer's refund address on request \u2014 contact support with the session id and transaction hash.\n- **The native coin cannot be recovered.** ETH, BNB, POL, TRX, SOL or any other native coin sent to a deposit address is not detected, not returned, and cannot be recovered by BitPal. Tell buyers to send only the token shown.\n- **Dust is not refunded.** A leftover balance of 0.0001 token or less (for example 0.0001 USDC) is not refunded automatically \u2014 the network fee would cost more than the amount. This applies to every automatic refund above.\n\n### 2. Refunding a completed payment (from the console)\n\nLike an app store, a completed payment is refunded by you, in the BitPal console:\n\n1. Open **Console \u2192 Sessions \u2192 the session \u2192 Refund**. It shows the network, token, the buyer's refund address (entered at payment time) and how much is still refundable (received minus refunds so far). Partial refunds are fine.\n2. **Send the refund from your own wallet** \u2014 one click with a connected EVM wallet, or copy the address, amount and token contract (with a QR code) for Tron, Solana or any other wallet.\n3. **Record the transaction hash.** BitPal checks it on-chain (confirmed, successful, the session's token sent to the refund address, after the payment, not more than what is left) and records it. You get `checkout.refund.recorded` with `kind: \"merchant\"`, the `reason_code` you picked and `from_address`; `GET /v1/checkout/sessions/{id}/refunds` lists it.\n\nCurrent policy: you pay the network fee and the refund amount; the BitPal fee on the original payment is not returned. Recording is console-only \u2014 an API key gets `403 REFUND_CONSOLE_ONLY`. Recording the same transaction again returns the existing record.\n\nTreat `kind` as open: new values may be added. Handle unknown kinds as a generic refund.\n\n## Reconciliation and reports\n\nTo match BitPal against your books, pull a report for a date range with your API key. There are three, each keyed on a different date:\n\n| Endpoint | Rows |\n| --- | --- |\n| `GET /v1/checkout/reports/payments` | Every session **created** in the range, in any status \u2014 with `deposit_tx_hash`, `sweep_tx_hash` and `payer_wallet`. |\n| `GET /v1/checkout/reports/payouts` | Every session **settled** to your payout wallet in the range (`settled_at`) \u2014 what reached your wallet, with the sweep transaction. |\n| `GET /v1/checkout/reports/fees` | Every session **paid** in the range (`paid_at`), with the platform fee. |\n\n- `from` / `to` are **UTC dates** (`YYYY-MM-DD`), both inclusive. Only the API key's mode is included \u2014 a test key never sees live payments.\n- **JSON** (default): one page, newest first (`limit` 1\u2013100, default 50, and `offset`), `meta.total`, and a `summary` with the count and amount, fee and net totals for the **whole range**.\n- **CSV** (`format=csv`): the whole range in one RFC 4180 file (quoted where needed, CRLF line endings) with the same columns as the JSON rows. Empty cells mean null.\n- Every row has your session `metadata` (for example your order id), without BitPal's reserved keys; in CSV it is one JSON-string column.\n- `amount_total`, `fee_amount` and `net_amount` are 6-decimal integer strings. `fee_amount_token` and `net_amount_token` are in the payment token's decimals (`token_decimals`) and match the on-chain transfers exactly \u2014 use them to reconcile against your wallet.\n\n```\n// Node SDK\nconst { data, meta, summary } = await bitpal.checkout.reports.payouts({ from: '2026-11-01', to: '2026-11-30' });\nconst csv = await bitpal.checkout.reports.csv('payouts', { from: '2026-11-01', to: '2026-11-30' });\n\n# curl\ncurl -H \"Authorization: Bearer $BITPAL_API_KEY\" \\\n \"https://api.bitpal.io/v1/checkout/reports/payouts?from=2026-11-01&to=2026-11-30&format=csv\"\n```\n\nThe console's Reports page downloads the same CSV files."
|
|
4091
|
+
markdown: "# Payment lifecycle\n\nWhat a session goes through, what each state means for you, and how amounts, late payments and refunds work.\n\n## States\n\n```\ncreated \u2500\u2500\u25B6 awaiting_deposit \u2500\u2500\u25B6 deposit_detected \u2500\u2500\u25B6 paid_confirmed \u2500\u2500\u25B6 swept\n \u2502 \u2502 \u2502\n \u2502 \u2514\u2500\u25B6 unresolved_underpaid \u2500\u2500(deadline)\u2500\u2500\u25B6 expired (partial refund)\n \u2514\u2500\u2500(deadline, nothing paid)\u2500\u2500\u25B6 expired\n```\n\n| Status | Meaning | What you do |\n| --- | --- | --- |\n| `created` | Session exists; the buyer has not picked a payment option yet. | Nothing. |\n| `awaiting_deposit` | A deposit address was issued; waiting for funds. | Nothing. |\n| `deposit_detected` | Funds seen on-chain, waiting for confirmations. | Show \"processing\". **Don't fulfil yet.** |\n| `paid_confirmed` | The full amount is confirmed. | **Fulfil** (webhook `checkout.session.paid`). |\n| `swept` | Funds moved to your payout wallet, minus the fee. | Reconcile (webhook `checkout.session.settled`). |\n| `unresolved_underpaid` | Less than the amount arrived. The buyer can top up until the deadline. | Wait. If it is still short at the deadline, it is refunded and the session expires. |\n| `wrong_token` | Legacy: sessions before 2026-11-12 that received another stablecoin BitPal supports on that network. Still payable with the right token. New sessions stay `awaiting_deposit` instead \u2014 the returned token shows up as a `wrong_token` refund (see Refunds). | Nothing. |\n| `expired` | The payment window closed without a full payment. | Release the order. A full payment can still arrive later \u2014 see Late payments. |\n\n> **Chain reorganisations.** Rarely, a confirmed deposit disappears after a chain reorg. You then receive `checkout.session.unresolved` with `reason: \"reorg\"` and the session goes back to `awaiting_deposit` (or `expired` past the deadline). If you have already fulfilled, treat it like a reversed payment.\n\n## Amounts\n\n| Field | Unit |\n| --- | --- |\n| `line_items[].amount` (input) | Plain number string in the line item's `currency`: `\"29\"`, `\"0.5\"`. `USDC`/`USDT` = USD, `JPYC` = JPY. No FX conversion. |\n| `amount_atomic` (input), `amount_total` | Always a **6-decimal integer string**, whatever token the buyer pays with: `\"29000000\"` = 29. |\n| `expectedAmount`, `pay_amount`, `amount_received`, webhook `amount_expected`/`amount_received`, refund `amount` | Integer string in the **pay token's own decimals** (`token_decimals`: 6 for USDC/USDT on most chains, 18 for BNB Chain USDT/USDC and JPYC). |\n| `fee_amount`, `net_amount` | 6-decimal integer strings; `fee_rate` is a decimal fraction (`\"0.015\"` = 1.5%). |\n\nHandle all amounts as strings or big integers \u2014 never as floating-point numbers.\n\n**Minimum amount.** A session's total (sum of `amount \xD7 quantity`) must be at least **1.00 in the session currency** (USDC/USDT: $1, JPYC: \xA51) \u2014 a lower total returns `400 CHECKOUT_AMOUNT_TOO_LOW`. The schema can't express this (it is a rule on the sum, and `amount` alone may be `\"0\"`), so check it before calling. Per-network minimums come on top: Tron currently $300 (testnet included) and JPYC \xA5100 on every network. A network whose minimum is above the total is not offered to the buyer; requesting a deposit address on it returns `400 CHECKOUT_BELOW_MIN_PAYMENT`.\n\n## Session fields worth knowing\n\n- `deposit_tx_hash` is the buyer's payment into the deposit address; `sweep_tx_hash` is the transaction that settled it to your payout wallet (or, for a refunded or rescued session, that on-chain action).\n- `tx_hash` is **deprecated**. It belonged to an earlier payment path and is kept only for compatibility \u2014 it now returns the same value as `deposit_tx_hash`. Use `deposit_tx_hash` / `sweep_tx_hash`.\n- `received_token` is the **address** of the token contract (or Solana mint) that actually arrived \u2014 not a symbol. `received_token_symbol` gives the symbol (`USDC`, `USDT` \u2026), or `null` for a token BitPal doesn't know (for example a wrong token).\n- `payer_wallet` is the address the deposit came from (or the wallet the buyer connected), in the network's own format \u2014 Tron addresses are base58 `T\u2026`. It may be an exchange's hot wallet, so don't use it as the buyer's identity or refund destination; refunds go to `refund_address`.\n- `metadata` is returned on the session as you sent it, and is also included in every webhook's `data` (see Webhooks \u2192 Event data). Keys BitPal writes itself \u2014 `_bitpal`, `payer_email`, `allowed_pay_chains`, `allowed_assets` \u2014 are reserved and are left out of both; don't use them for your own data.\n\n## Buyer experience\n\n- **Receipts.** BitPal does not email the buyer automatically. After paying, the buyer can request an emailed receipt on the payment completion screen; `customer_email` only pre-fills that form. If every buyer needs a receipt or order confirmation, send it yourself from your `checkout.session.paid` handler.\n- **Status link.** `checkout_url` (`/pay/{id}?token=\u2026`) is also the buyer's status page \u2014 opening it later shows the payment's current state, and the receipt email links back to it. The `token` grants access to that page, so share the URL only with the buyer.\n- **Language.** The payment page picks its language from the buyer's browser (12 languages, English as fallback), and the buyer can switch it. You can't set the language from the session or the URL yet.\n- **Your name and logo.** The payment page does not show a merchant name or logo yet. Buyers see the line item `name`s and amounts \u2014 make item names recognisable (for example include your store name).\n- **Support.** BitPal doesn't contact your buyers. Product, order and refund questions are yours to answer (completed payments are refunded by you \u2014 see Refunds above). For payment problems \u2014 funds sent but not detected, a wrong network, a late transfer \u2014 contact BitPal support with the session id and the transaction hash.\n\n## Payment window and late payments\n\n- The buyer has `expires_in_minutes` (1\u201360, default 15) from session creation to pay. That deadline is the session's `expires_at`.\n- `late` compares the **block time of the deposit transaction** with `expires_at`. A transfer sent in time is `\"late\": false` even if its confirmations finish after the deadline; `checkout.session.settled` carries the same value as `paid`.\n- Once a deposit address has been issued, BitPal keeps the session open for **90 minutes after `expires_at`** so transfers already in flight can confirm. Underpayment refunds and `checkout.session.expired` for an issued session happen at the end of that window \u2014 so for such a session the `expired` event arrives about 90 minutes after `expires_at`, not at `expires_at`.\n- A session that never got a deposit address (the buyer never picked a network) expires at `expires_at`: BitPal marks it expired within about 5 minutes, or immediately when the session is read, and sends `checkout.session.expired` with `reason: \"no_deposit\"` then.\n- **Holding stock?** If you cannot keep an item reserved for the extra 90 minutes, release it yourself at `expires_at` instead of waiting for the event. A full payment that still arrives is then reported as `checkout.session.paid` with `\"late\": true` (or refunded, with `\"late_payments\": \"refund\"`), so be ready to fulfil or refund it.\n- **A full payment that arrives after the deadline is accepted as a late payment** for up to 7 days by default: you get `checkout.session.paid` with `\"late\": true` and the funds settle normally. Decide whether to fulfil, or refund it from the console (see Refunds below).\n- **Don't want late payments?** Create the session with `\"late_payments\": \"refund\"`. A full payment that arrives after the deadline (`expires_at` + 90 minutes) is then never accepted: it stays in the deposit address and is refunded automatically to the buyer's refund address **about 7 days after the deadline** (refund kind `late_deposit`, `checkout.refund.recorded`). The refund can't be instant \u2014 BitPal is non-custodial and the deposit contract only lets anyone but you take the funds back after that 7-day window. As soon as such a payment is seen you get `checkout.session.unresolved` with `reason: \"late\"` and `refund_available_at` (once), so you can tell the buyer. A transfer sent before `expires_at` whose confirmations finish later is still accepted.\n- This only applies to a session with no earlier on-chain action. If a wrong token was already returned or an underpayment already refunded, the expired session is closed for good: later deposits to its address are not accepted as payment (contact support with the session id and transaction hash).\n- Acceptance ends 1 hour before the on-chain refund window opens (`expires_at` + 90 minutes + 7 days). A full payment that arrives after that cutoff is not accepted; it is refunded automatically once the window opens (refund kind `late_deposit`).\n- A **partial** payment after the deadline is refunded automatically (refund kind `late_deposit`).\n- Funds sent more than 7 days after the deadline are not watched automatically. Contact support with the session id and transaction hash.\n\n## Refunds\n\n**BitPal is non-custodial.** A payment goes from the buyer to a one-time deposit address and then straight to your payout wallet \u2014 BitPal never holds it and has no right to pull it back. So there is **no refund API**. There are two kinds of refund, and both fire `checkout.refund.recorded`:\n\n### 1. Automatic refunds (mismatched payments)\n\nPayments that don't match the order are returned on-chain from the deposit address to the buyer's refund address \u2014 automatically, with no action from you.\n\n| `kind` | When | Session outcome |\n| --- | --- | --- |\n| `overpay_excess` | More than the amount arrived. | Paid; the exact amount settles, the excess is refunded. |\n| `underpay_expired` | Still short at the deadline. | Expired; everything received is refunded. |\n| `wrong_token` | Another stablecoin that BitPal supports on that network was sent instead of the selected one. | Unchanged; still payable with the right token. |\n| `late_deposit` | A partial payment arrived after the deadline, or a full payment arrived after the late-acceptance cutoff (see above). | Stays expired. |\n\n- **Only supported stablecoins are returned automatically** \u2014 the tokens BitPal supports on that network (see [Chains & contracts](/docs/md/reference.md)). Any other token is not watched: it is not returned automatically and the session does not change. On EVM networks and Tron it can be returned to the buyer's refund address on request \u2014 contact support with the session id and transaction hash.\n- **The native coin cannot be recovered.** ETH, BNB, POL, TRX, SOL or any other native coin sent to a deposit address is not detected, not returned, and cannot be recovered by BitPal. Tell buyers to send only the token shown.\n- **Dust is not refunded.** A leftover balance of 0.0001 token or less (for example 0.0001 USDC) is not refunded automatically \u2014 the network fee would cost more than the amount. This applies to every automatic refund above.\n\n### 2. Refunding a completed payment (from the console)\n\nLike an app store, a completed payment is refunded by you, in the BitPal console:\n\n1. Open **Console \u2192 Sessions \u2192 the session \u2192 Refund**. It shows the network, token, the buyer's refund address (entered at payment time) and how much is still refundable (received minus refunds so far). Partial refunds are fine.\n2. **Send the refund from your own wallet** \u2014 usually the payout wallet the payment settled to. One click with a connected EVM wallet (**Connect wallet** links MetaMask and others even if you signed in with Google; BitPal checks the balance first), or copy the address, amount and token contract (with a QR code) for Tron, Solana or any other wallet.\n3. **Record the transaction hash.** BitPal checks it on-chain (confirmed, successful, the session's token sent to the refund address, after the payment, not more than what is left) and records it. You get `checkout.refund.recorded` with `kind: \"merchant\"`, the `reason_code` you picked and `from_address`; `GET /v1/checkout/sessions/{id}/refunds` lists it.\n\nAfter a refund the session `status` stays `swept` (settled) \u2014 the payment itself happened. Read `refund_status` on the session (`none` \xB7 `partially_refunded` \xB7 `refunded`; only refunds you recorded count) \u2014 the console shows it as a badge next to the status.\n\nCurrent policy: you pay the network fee and the refund amount; the BitPal fee on the original payment is not returned. Recording is console-only \u2014 an API key gets `403 REFUND_CONSOLE_ONLY`. Recording the same transaction again returns the existing record.\n\nTreat `kind` as open: new values may be added. Handle unknown kinds as a generic refund.\n\n## Reconciliation and reports\n\nTo match BitPal against your books, pull a report for a date range with your API key. There are three, each keyed on a different date:\n\n| Endpoint | Rows |\n| --- | --- |\n| `GET /v1/checkout/reports/payments` | Every session **created** in the range, in any status \u2014 with `deposit_tx_hash`, `sweep_tx_hash` and `payer_wallet`. |\n| `GET /v1/checkout/reports/payouts` | Every session **settled** to your payout wallet in the range (`settled_at`) \u2014 what reached your wallet, with the sweep transaction. |\n| `GET /v1/checkout/reports/fees` | Every session **paid** in the range (`paid_at`), with the platform fee. |\n\n- `from` / `to` are **UTC dates** (`YYYY-MM-DD`), both inclusive. Only the API key's mode is included \u2014 a test key never sees live payments.\n- **JSON** (default): one page, newest first (`limit` 1\u2013100, default 50, and `offset`), `meta.total`, and a `summary` with the count and amount, fee and net totals for the **whole range**.\n- **CSV** (`format=csv`): the whole range in one RFC 4180 file (quoted where needed, CRLF line endings) with the same columns as the JSON rows. Empty cells mean null.\n- Every row has your session `metadata` (for example your order id), without BitPal's reserved keys; in CSV it is one JSON-string column.\n- `amount_total`, `fee_amount` and `net_amount` are 6-decimal integer strings. `fee_amount_token` and `net_amount_token` are in the payment token's decimals (`token_decimals`) and match the on-chain transfers exactly \u2014 use them to reconcile against your wallet.\n\n```\n// Node SDK\nconst { data, meta, summary } = await bitpal.checkout.reports.payouts({ from: '2026-11-01', to: '2026-11-30' });\nconst csv = await bitpal.checkout.reports.csv('payouts', { from: '2026-11-01', to: '2026-11-30' });\n\n# curl\ncurl -H \"Authorization: Bearer $BITPAL_API_KEY\" \\\n \"https://api.bitpal.io/v1/checkout/reports/payouts?from=2026-11-01&to=2026-11-30&format=csv\"\n```\n\nThe console's Reports page downloads the same CSV files."
|
|
4092
4092
|
},
|
|
4093
4093
|
{
|
|
4094
4094
|
slug: "links",
|
|
@@ -4102,35 +4102,35 @@ var docs_data_default = {
|
|
|
4102
4102
|
title: "Accept crypto. Get paid directly.",
|
|
4103
4103
|
url: "https://bitpal.io/docs/#overview",
|
|
4104
4104
|
markdownUrl: "https://bitpal.io/docs/md/overview.md",
|
|
4105
|
-
markdown: "Non-custodial crypto checkout\n\n# Accept crypto. Get paid directly.\n\nEach order gets its own deposit address. The buyer sends stablecoins from any wallet or exchange \u2014 no wallet connection \u2014 and funds settle straight to you. No escrow, no custody.\n\n> **New here?** Take a test payment end to end in about ten minutes \u2192 [Quickstart](/docs/md/quickstart.md).\n\n## Three ways to integrate\n\n- **SDK / hosted checkout** \u2014 create a session in code, redirect the buyer to the returned URL. \u2192 [SDK](/docs/md/sdk.md)\n- **Payment links** \u2014 no code. Create a link, share the URL, get paid. \u2192 [Payment links](/docs/md/links.md)\n- **x402** \u2014 charge per API call. The caller pays with a signed authorization and retries, no page involved. \u2192 [x402](/docs/md/x402.md)\n\n## What you can accept\n\n| Token | Networks |\n| --- | --- |\n| **USDC** | Base \xB7 Arbitrum \xB7 Solana |\n| **USDT** |
|
|
4105
|
+
markdown: "Non-custodial crypto checkout\n\n# Accept crypto. Get paid directly.\n\nEach order gets its own deposit address. The buyer sends stablecoins from any wallet or exchange \u2014 no wallet connection \u2014 and funds settle straight to you. No escrow, no custody.\n\n> **New here?** Take a test payment end to end in about ten minutes \u2192 [Quickstart](/docs/md/quickstart.md).\n\n## Three ways to integrate\n\n- **SDK / hosted checkout** \u2014 create a session in code, redirect the buyer to the returned URL. \u2192 [SDK](/docs/md/sdk.md)\n- **Payment links** \u2014 no code. Create a link, share the URL, get paid. \u2192 [Payment links](/docs/md/links.md)\n- **x402** \u2014 charge per API call. The caller pays with a signed authorization and retries, no page involved. \u2192 [x402](/docs/md/x402.md)\n\n## What you can accept\n\n| Token | Networks |\n| --- | --- |\n| **USDC** | Base \xB7 Arbitrum \xB7 Optimism \xB7 BNB (BSC) \xB7 Polygon \xB7 Solana |\n| **USDT** | BNB (BSC) \xB7 Polygon \xB7 Solana \xB7 Tron (coming soon) |\n| **JPYC** | Polygon \xB7 Kaia |\n\nSame-chain, same-token \u2014 no bridging or swapping. Full identifiers & decimals in [Chains & contracts](/docs/md/reference.md).\n\n## How a payment flows\n\n1. You create a session (or link) with an amount.\n2. Buyer opens the hosted page, picks a network, sends the exact amount to a unique address.\n3. BitPal detects & confirms it, settles to you, and sends a `checkout.session.paid` webhook."
|
|
4106
4106
|
},
|
|
4107
4107
|
{
|
|
4108
4108
|
slug: "quickstart",
|
|
4109
4109
|
title: "Quickstart",
|
|
4110
4110
|
url: "https://bitpal.io/docs/#quickstart",
|
|
4111
4111
|
markdownUrl: "https://bitpal.io/docs/md/quickstart.md",
|
|
4112
|
-
markdown: '# Quickstart\n\nTake one test payment end to end: create a session, pay it on a testnet, and receive the signed webhook. About ten minutes.\n\n## Before you start\n\n1. **Console account in test mode.** Everything below uses testnets and test keys \u2014 no real funds.\n2. **Payout wallet.** Console \u2192 Settings \u2192 **Payout Wallet**. Each deposit address is committed to this wallet, so a session cannot issue an address without one. Register an address for every network family you accept (EVM `0x\u2026`, Tron `T\u2026`, Solana) \u2014 see **Payout wallets** below.\n3. **Test API key.** Console \u2192 Developers \u2192 **API Keys** \u2192 create a `bp_test_\u2026` key. It is shown once. Keep it on your server \u2014 never in a browser or app bundle.\n4. **A testnet wallet with a little gas** (for example Base Sepolia ETH) to send the payment. The checkout page\'s test faucet mints test tokens, not gas.\n\n## Payout wallets\n\nPayments settle straight to your own wallet \u2014 BitPal never holds them. You register one payout address per **network family** (VM) in **Console \u2192 Settings \u2192 Payout Wallet**. There is no API for this; it is console-only.\n\n| Network family | Address to register | Used for |\n| --- | --- | --- |\n| EVM | One `0x\u2026` address | Every EVM network you accept (Base, Arbitrum, BNB Chain, Polygon, Kaia \u2026) \u2014 the same address works on all of them. |\n| Tron | A base58 address starting with `T\u2026` | Tron payments. |\n| Solana | Your **wallet (owner) address** \u2014 not a token account (ATA) | Solana payments. |\n\n- **Test and live are separate.** Wallets are registered per mode: switch the console to TEST to register test payout wallets, and to LIVE for live ones. A test wallet is never used for live payments, or the other way round.\n- **A family without a wallet is not offered.** If the buyer can choose, networks of a family with no payout wallet in that mode are left out of the payment options. If you fix the network with `pay_chain` (or a deposit address is requested on it), the call fails with `400 SELLER_WALLET_NOT_CONFIGURED`. There is no fallback to another family or another wallet. A session created without `pay_chain` needs at least one payout wallet (any family) in that mode \u2014 if `allowed_pay_chains` / `allowed_assets` are set, a wallet for one of the listed networks\' families. Without any network restriction, a session offers Base USDC if you have an EVM wallet, otherwise the networks of the families you registered.\n- **Changing a wallet** applies to deposit addresses issued afterwards. An address that was already issued is committed to the payout wallet of that moment (the commitment is part of the address), so a payment to it still settles there.\n\n## 1. Create a checkout session\n\nInstall the SDK, or call the REST API directly.\n\n```\nnpm install @bitpal/checkout\n```\n\n```\nimport { BitPal } from \'@bitpal/checkout\';\n\nconst bitpal = new BitPal(process.env.BITPAL_API_KEY); // bp_test_\u2026\n\nconst { data: session } = await bitpal.checkout.createSession({\n line_items: [{ name: \'Pro plan\', amount: \'29\', currency: \'USDC\' }],\n pay_chain: \'eip155:84532\', // Base Sepolia (test key \u2192 testnet values only)\n expires_in_minutes: 30, // payment window, 1\u201360 (default 15)\n success_url: \'https://your.store/thanks?session={CHECKOUT_SESSION_ID}\',\n});\n// store session.id with your order, then send the buyer to session.checkout_url\n```\n\nThe same request over HTTP:\n\n```\ncurl -X POST https://api.bitpal.io/v1/checkout/sessions \\\n -H "Authorization: Bearer $BITPAL_API_KEY" \\\n -H "Content-Type: application/json" \\\n -H "Idempotency-Key: order-1024" \\\n -d \'{\n "line_items": [{ "name": "Pro plan", "amount": "29", "currency": "USDC" }],\n "pay_chain": "eip155:84532",\n "expires_in_minutes": 30,\n "success_url": "https://your.store/thanks?session={CHECKOUT_SESSION_ID}"\n }\'\n```\n\nResponse `201`:\n\n```\n{\n "data": {\n "id": "cs_Q2FxKhomCEju_OqvKfrQsTOh",\n "checkout_url": "https://bitpal.io/pay/cs_Q2FxKhomCEju_OqvKfrQsTOh?token=<session_token>",\n "session_token": "<session_token>",\n "status": "created",\n "amount_total": "29000000",\n "currency": "USDC",\n "expires_at": "2026-11-06T07:58:28.776Z",\n "tax": { "tax_amount": "0", "total_with_tax": "29000000" }\n },\n "meta": { "mode": "TEST", "requestId": "ec091faf-\u2026" }\n}\n```\n\n- `amount` is a plain number string in the line item\'s currency \u2014 no decimals to compute. `amount_total` comes back as a 6-decimal integer string (`"29000000"` = 29).\n- **Minimum:** the session total (sum of `amount \xD7 quantity`) must be at least **1.00 in the session currency** ($1 for USDC/USDT, \xA51 for JPYC), otherwise `400 CHECKOUT_AMOUNT_TOO_LOW`. Some networks have a higher minimum \u2014 Tron currently $300 (testnet included), JPYC \xA5100 on every network; such a network is not offered for a smaller total (or `400 CHECKOUT_BELOW_MIN_PAYMENT` if you fixed it with `pay_chain`).\n- `Idempotency-Key` (8\u2013128 chars) makes retries safe: the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`.\n- `{CHECKOUT_SESSION_ID}` in `success_url` is replaced with the session id when the buyer returns.\n\n## 2. Send the buyer to the checkout page\n\nRedirect to `checkout_url`, or open it in a popup so the buyer stays on your page (see [Popup & headless](/docs/md/flows.md)). On the page the buyer:\n\n1. picks a network and token (if you allowed more than one),\n2. enters a **refund address** \u2014 over/under-payments are returned there,\n3. gets a one-time **deposit address** and the exact amount, and sends it from any wallet or exchange.\n\nIn test mode the page shows **Get test assets**: it mints test tokens to the buyer\'s wallet so you can complete a real on-chain payment.\n\n> **No testnet wallet?** Console \u2192 Developers \u2192 **Sandbox** creates a session and sends the deposit for you \u2014 pay in full, underpay, overpay or send the wrong token \u2014 and shows every webhook event as your endpoint receives it. Test mode only; 10 simulations per minute and 300 per day per account.\n\n## 3. Receive the webhook\n\nConsole \u2192 Developers \u2192 Webhooks \u2192 **Add Endpoint**. The URL must be publicly reachable over HTTPS (for local development use a tunnel such as ngrok or cloudflared). Copy the signing secret (`whsec_\u2026`) \u2014 it is shown once.\n\n```\nimport express from \'express\';\nimport { verifyWebhookSignature, parseWebhookEvent, WEBHOOK_HEADERS } from \'@bitpal/checkout\';\n\nconst app = express();\n\napp.post(\'/webhooks/bitpal\', express.raw({ type: \'application/json\' }), async (req, res) => {\n const raw = req.body.toString(\'utf8\'); // exact bytes BitPal signed\n const ok = await verifyWebhookSignature( // async \u2014 always await it\n raw,\n req.header(WEBHOOK_HEADERS.signature) ?? \'\',\n process.env.BITPAL_WEBHOOK_SECRET, // whsec_\u2026\n { timestamp: req.header(WEBHOOK_HEADERS.timestamp) ?? \'\' },\n );\n if (!ok) return res.status(400).send(\'invalid signature\');\n\n const event = parseWebhookEvent(raw);\n if (await alreadyProcessed(event.id)) return res.sendStatus(200); // at-least-once delivery\n\n if (event.event === \'checkout.session.paid\') {\n await fulfilOrder(event.data.session_id, { late: event.data.late === true });\n }\n await markProcessed(event.id);\n res.sendStatus(200); // any 2xx within 10 s = delivered\n});\n```\n\nFor one successful payment you receive, in order: `checkout.session.created` \u2192 `checkout.session.deposit_detected` \u2192 `checkout.session.paid` \u2192 `checkout.session.settled`. `deposit_detected` is not guaranteed: `paid` can arrive without it (a late payment, or a deposit that was already fully confirmed when BitPal first saw it), so never wait for it before fulfilling. Use **Send Test** in the console to check your endpoint before paying.\n\n## 4. Fulfil on `checkout.session.paid`\n\n- `paid` means the deposit reached the required confirmations \u2014 safe to fulfil. `settled` follows when funds are swept to your payout wallet.\n- Treat the webhook (or a server-side `getSession(id)`) as the source of truth. The redirect to `success_url` and popup messages are hints for your UI only.\n- Check `data.late`: `true` means the full amount arrived after the payment window (accepted for up to 7 days). Decide whether to fulfil or refund it from the console \u2014 or create sessions with `"late_payments": "refund"` so late payments are refunded automatically instead ([Payment lifecycle](/docs/md/lifecycle.md)).\n\n## Going live\n\n1. Create a `bp_live_\u2026` key and switch chain values to mainnet ([Chains & contracts](/docs/md/reference.md)).\n2. Register live payout wallets \u2014 test and live are separate environments.\n3. Add the webhook endpoint again in live mode. It gets its own `whsec_\u2026` secret.\n4. Handle the non-happy paths: `checkout.session.unresolved`, `checkout.session.expired`, `checkout.refund.recorded` and late payments ([Payment lifecycle](/docs/md/lifecycle.md)).'
|
|
4112
|
+
markdown: '# Quickstart\n\nTake one test payment end to end: create a session, pay it on a testnet, and receive the signed webhook. About ten minutes.\n\n## Before you start\n\n1. **Console account in test mode.** Everything below uses testnets and test keys \u2014 no real funds.\n2. **Payout wallet.** Console \u2192 Settings \u2192 **Payout Wallet**. Each deposit address is committed to this wallet, so a session cannot issue an address without one. Register an address for every network family you accept (EVM `0x\u2026`, Tron `T\u2026`, Solana) \u2014 see **Payout wallets** below.\n3. **Test API key.** Console \u2192 Developers \u2192 **API Keys** \u2192 create a `bp_test_\u2026` key. It is shown once. Keep it on your server \u2014 never in a browser or app bundle.\n4. **A testnet wallet with a little gas** (for example Base Sepolia ETH) to send the payment. The checkout page\'s test faucet mints test tokens, not gas.\n\n## Payout wallets\n\nPayments settle straight to your own wallet \u2014 BitPal never holds them. You register one payout address per **network family** (VM) in **Console \u2192 Settings \u2192 Payout Wallet**. There is no API for this; it is console-only.\n\n| Network family | Address to register | Used for |\n| --- | --- | --- |\n| EVM | One `0x\u2026` address | Every EVM network you accept (Base, Arbitrum, BNB Chain, Polygon, Kaia \u2026) \u2014 the same address works on all of them. |\n| Tron | A base58 address starting with `T\u2026` | Tron payments. |\n| Solana | Your **wallet (owner) address** \u2014 not a token account (ATA) | Solana payments. |\n\n- **Test and live are separate.** Wallets are registered per mode: switch the console to TEST to register test payout wallets, and to LIVE for live ones. A test wallet is never used for live payments, or the other way round.\n- **A family without a wallet is not offered.** If the buyer can choose, networks of a family with no payout wallet in that mode are left out of the payment options. If you fix the network with `pay_chain` (or a deposit address is requested on it), the call fails with `400 SELLER_WALLET_NOT_CONFIGURED`. There is no fallback to another family or another wallet. A session created without `pay_chain` needs at least one payout wallet (any family) in that mode \u2014 if `allowed_pay_chains` / `allowed_assets` are set, a wallet for one of the listed networks\' families. Without any network restriction, a session offers Base USDC if you have an EVM wallet, otherwise the networks of the families you registered.\n- **Changing a wallet** applies to deposit addresses issued afterwards. An address that was already issued is committed to the payout wallet of that moment (the commitment is part of the address), so a payment to it still settles there.\n\n## 1. Create a checkout session\n\nInstall the SDK, or call the REST API directly.\n\n```\nnpm install @bitpal/checkout\n```\n\n```\nimport { BitPal } from \'@bitpal/checkout\';\n\nconst bitpal = new BitPal(process.env.BITPAL_API_KEY); // bp_test_\u2026\n\nconst { data: session } = await bitpal.checkout.createSession({\n line_items: [{ name: \'Pro plan\', amount: \'29\', currency: \'USDC\' }],\n pay_chain: \'eip155:84532\', // Base Sepolia (test key \u2192 testnet values only)\n expires_in_minutes: 30, // payment window, 1\u201360 (default 15)\n success_url: \'https://your.store/thanks?session={CHECKOUT_SESSION_ID}\',\n});\n// store session.id with your order, then send the buyer to session.checkout_url\n```\n\nThe same request over HTTP:\n\n```\ncurl -X POST https://api.bitpal.io/v1/checkout/sessions \\\n -H "Authorization: Bearer $BITPAL_API_KEY" \\\n -H "Content-Type: application/json" \\\n -H "Idempotency-Key: order-1024" \\\n -d \'{\n "line_items": [{ "name": "Pro plan", "amount": "29", "currency": "USDC" }],\n "pay_chain": "eip155:84532",\n "expires_in_minutes": 30,\n "success_url": "https://your.store/thanks?session={CHECKOUT_SESSION_ID}"\n }\'\n```\n\nResponse `201`:\n\n```\n{\n "data": {\n "id": "cs_Q2FxKhomCEju_OqvKfrQsTOh",\n "checkout_url": "https://bitpal.io/pay/cs_Q2FxKhomCEju_OqvKfrQsTOh?token=<session_token>",\n "session_token": "<session_token>",\n "status": "created",\n "amount_total": "29000000",\n "currency": "USDC",\n "expires_at": "2026-11-06T07:58:28.776Z",\n "tax": { "tax_amount": "0", "total_with_tax": "29000000" }\n },\n "meta": { "mode": "TEST", "requestId": "ec091faf-\u2026" }\n}\n```\n\n- `amount` is a plain number string in the line item\'s currency \u2014 no decimals to compute. `amount_total` comes back as a 6-decimal integer string (`"29000000"` = 29).\n- **Minimum:** the session total (sum of `amount \xD7 quantity`) must be at least **1.00 in the session currency** ($1 for USDC/USDT, \xA51 for JPYC), otherwise `400 CHECKOUT_AMOUNT_TOO_LOW`. Some networks have a higher minimum \u2014 Tron currently $300 (testnet included), JPYC \xA5100 on every network; such a network is not offered for a smaller total (or `400 CHECKOUT_BELOW_MIN_PAYMENT` if you fixed it with `pay_chain`).\n- `Idempotency-Key` (8\u2013128 chars) makes retries safe: the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`.\n- `{CHECKOUT_SESSION_ID}` in `success_url` is replaced with the session id when the buyer returns.\n\n## 2. Send the buyer to the checkout page\n\nRedirect to `checkout_url`, or open it in a popup so the buyer stays on your page (see [Popup & headless](/docs/md/flows.md)). On the page the buyer:\n\n1. picks a network and token (if you allowed more than one),\n2. enters a **refund address** \u2014 over/under-payments are returned there,\n3. gets a one-time **deposit address** and the exact amount, and sends it from any wallet or exchange.\n\nIn test mode the page shows **Get test assets**: it mints test tokens to the buyer\'s wallet so you can complete a real on-chain payment.\n\n> **No testnet wallet?** Console \u2192 Developers \u2192 **Sandbox** creates a session and sends the deposit for you \u2014 pay in full, underpay, overpay or send the wrong token (BNB Smart Chain Testnet and Polygon Amoy, which have a second test token) \u2014 and shows every webhook event as your endpoint receives it. To test a late payment, let a session with an issued deposit address expire, then simulate again. Test mode only; 10 simulations per minute and 300 per day per account.\n\n## 3. Receive the webhook\n\nConsole \u2192 Developers \u2192 Webhooks \u2192 **Add Endpoint**. The URL must be publicly reachable over HTTPS (for local development use a tunnel such as ngrok or cloudflared). Copy the signing secret (`whsec_\u2026`) \u2014 it is shown once.\n\n```\nimport express from \'express\';\nimport { verifyWebhookSignature, parseWebhookEvent, WEBHOOK_HEADERS } from \'@bitpal/checkout\';\n\nconst app = express();\n\napp.post(\'/webhooks/bitpal\', express.raw({ type: \'application/json\' }), async (req, res) => {\n const raw = req.body.toString(\'utf8\'); // exact bytes BitPal signed\n const ok = await verifyWebhookSignature( // async \u2014 always await it\n raw,\n req.header(WEBHOOK_HEADERS.signature) ?? \'\',\n process.env.BITPAL_WEBHOOK_SECRET, // whsec_\u2026\n { timestamp: req.header(WEBHOOK_HEADERS.timestamp) ?? \'\' },\n );\n if (!ok) return res.status(400).send(\'invalid signature\');\n\n const event = parseWebhookEvent(raw);\n if (await alreadyProcessed(event.id)) return res.sendStatus(200); // at-least-once delivery\n\n if (event.event === \'checkout.session.paid\') {\n await fulfilOrder(event.data.session_id, { late: event.data.late === true });\n }\n await markProcessed(event.id);\n res.sendStatus(200); // any 2xx within 10 s = delivered\n});\n```\n\nFor one successful payment you receive, in order: `checkout.session.created` \u2192 `checkout.session.deposit_detected` \u2192 `checkout.session.paid` \u2192 `checkout.session.settled`. `deposit_detected` is not guaranteed: `paid` can arrive without it (a late payment, or a deposit that was already fully confirmed when BitPal first saw it), so never wait for it before fulfilling. Use **Send Test** in the console to check your endpoint before paying.\n\n## 4. Fulfil on `checkout.session.paid`\n\n- `paid` means the deposit reached the required confirmations \u2014 safe to fulfil. `settled` follows when funds are swept to your payout wallet.\n- Treat the webhook (or a server-side `getSession(id)`) as the source of truth. The redirect to `success_url` and popup messages are hints for your UI only.\n- Check `data.late`: `true` means the full amount arrived after the payment window (accepted for up to 7 days). Decide whether to fulfil or refund it from the console \u2014 or create sessions with `"late_payments": "refund"` so late payments are refunded automatically instead ([Payment lifecycle](/docs/md/lifecycle.md)).\n\n## Going live\n\n1. Create a `bp_live_\u2026` key and switch chain values to mainnet ([Chains & contracts](/docs/md/reference.md)).\n2. Register live payout wallets \u2014 test and live are separate environments.\n3. Add the webhook endpoint again in live mode. It gets its own `whsec_\u2026` secret.\n4. Handle the non-happy paths: `checkout.session.unresolved`, `checkout.session.expired`, `checkout.refund.recorded` and late payments ([Payment lifecycle](/docs/md/lifecycle.md)).'
|
|
4113
4113
|
},
|
|
4114
4114
|
{
|
|
4115
4115
|
slug: "reference",
|
|
4116
4116
|
title: "Chains & contracts",
|
|
4117
4117
|
url: "https://bitpal.io/docs/#reference",
|
|
4118
4118
|
markdownUrl: "https://bitpal.io/docs/md/reference.md",
|
|
4119
|
-
markdown:
|
|
4119
|
+
markdown: '# Chains & contracts\n\n## Chains & identifiers\n\nThe `pay_chain` / `allowed_pay_chains` values are [CAIP-2](https://chainagnostic.org/CAIPs/caip-2) identifiers. Use the value from the column matching your API key\'s mode.\n\n| Network | Tokens | Decimals | `pay_chain` \u2014 mainnet | `pay_chain` \u2014 testnet |\n| --- | --- | --- | --- | --- |\n| **Base** | USDC | 6 | `eip155:8453` | `eip155:84532` |\n| **Arbitrum** | USDC | 6 | `eip155:42161` | `eip155:421614` |\n| **Optimism** | USDC | 6 | `eip155:10` | `eip155:11155420` |\n| **BNB Smart Chain** | USDT \xB7 USDC | 18 \xB7 18 | `eip155:56` | `eip155:97` |\n| **Tron** | USDT | 6 | `tron:0x2b6653dc` (coming soon) | `tron:0xcd8690dc` |\n| **Solana** | USDC \xB7 USDT | 6 | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` |\n| **Polygon** | JPYC \xB7 USDC \xB7 USDT | 18 \xB7 6 \xB7 6 | `eip155:137` | `eip155:80002` |\n| **Kaia** | JPYC | 18 | `eip155:8217` | `eip155:1001` |\n\n> JPYC (Polygon/Kaia) settles 1:1 to JPY \u2014 no FX conversion.\n\n> Test keys (`bp_test_\u2026`) accept only testnet values; live keys only mainnet.\n\n## Contract addresses\n\nThe deposit-address contracts (EVM/Tron deterministic forwarder factory, Solana program). Published here as each network goes live.\n\n| Network | Contract / program | Mainnet | Testnet |\n| --- | --- | --- | --- |\n| Base | Forwarder factory | `0xC5d358f4346d59D8eCDb5cE7DE77367b068a7E7c` | `0x5bA4F84B3002B9F94670f296134aE4f823662B9f` |\n| Arbitrum | Forwarder factory | `0xC7989977010890AE78938a84E487573a1D993366` | `0x38B54b7F8a032E8087cd04f12a7Bdb9011c5Bd95` |\n| Optimism | Forwarder factory | `0x3Cfb82D593A6ba4D2CE2C17bD5A71A99D15797cC` | `0xC7989977010890AE78938a84E487573a1D993366` |\n| BNB | Forwarder factory | `0xC7989977010890AE78938a84E487573a1D993366` | `0xeD24d9ED87a9937528520413891e8f78EF7ED3C5` |\n| Tron | Forwarder factory | Coming soon | `TMwTqCDtWqA2jJ3Qii3hS2ADMSem4Dnubj` |\n| Solana | Program ID | `2cZyQxMgWToiCWho9q24VyAvs6xy7t3JSEEwNpGjf4ec` | `2cZyQxMgWToiCWho9q24VyAvs6xy7t3JSEEwNpGjf4ec` |\n| Polygon | Forwarder factory | `0xA2F1bB858DBc25355D8cb5180D7e58bDC2D696De` | `0x42A70E1a47C9AAeF10923f5345b2ad64D2F05D3F` |\n| Kaia | Forwarder factory | `0xA2F1bB858DBc25355D8cb5180D7e58bDC2D696De` | `0xf07BB5243636D8C834b31f224E2567E6e6aF38ce` |\n\nThese are the factories that derive deposit addresses. Always treat the deposit address returned by BitPal as authoritative for a given order \u2014 funds are cryptographically committed to your destination.\n\n## Refunds & edge cases\n\nPayments are push transfers, so a buyer can pay the wrong amount or token. BitPal resolves each case automatically to the buyer\'s refund address (set at pay time, committed & immutable).\n\n| Case | What happens |\n| --- | --- |\n| **Overpaid** | Settles the exact amount; surplus refunded. |\n| **Underpaid** | Open for a top-up; refunded if the deadline passes. |\n| **Wrong token (another supported stablecoin)** | Returned to the buyer; order stays open. |\n| **Unsupported token** | Not detected and not returned automatically; on EVM and Tron it can be returned on request (contact support). |\n| **Native coin (ETH, BNB, TRX, SOL \u2026)** | Not detected and cannot be recovered by BitPal. |\n| **Dust (0.0001 token or less)** | Not refunded automatically \u2014 the network fee would exceed it. |\n| **Expired** | No funds arrived \u2014 address reclaimed. |\n| **Late, full amount** | Accepted as a late payment for up to 7 days (`late: true`); settles normally. Not for sessions that already had a wrong-token return or underpayment refund; after the cutoff it is refunded (`late_deposit`). |\n| **Late, full amount, `late_payments: "refund"`** | Not accepted. `checkout.session.unresolved` (`reason: "late"`) right away, then refunded automatically about 7 days after the deadline (`late_deposit`) \u2014 the non-custodial deposit contract does not allow an earlier refund. |\n| **Late, partial** | Refunded (`late_deposit`). |\n| **Customer return (completed payment)** | No refund API \u2014 BitPal never holds the funds. Refund in Console \u2192 Sessions \u2192 session \u2192 Refund: send from your own wallet, record the transaction, and BitPal verifies it and sends `checkout.refund.recorded` (`merchant`). You pay the gas and the refund; the BitPal fee is not returned. |\n\n## Test mode & faucet\n\n- Keys are environment-scoped: `bp_test_\u2026` on testnets, `bp_live_\u2026` on mainnet.\n- In test mode the hosted page shows a **faucet** \u2014 the buyer mints mock tokens to run a real end-to-end payment. (Gas is separate; the faucet sends tokens only.)'
|
|
4120
4120
|
},
|
|
4121
4121
|
{
|
|
4122
4122
|
slug: "sdk",
|
|
4123
4123
|
title: "SDK",
|
|
4124
4124
|
url: "https://bitpal.io/docs/#sdk",
|
|
4125
4125
|
markdownUrl: "https://bitpal.io/docs/md/sdk.md",
|
|
4126
|
-
markdown: "# SDK\n\nCreate checkout sessions from your backend and redirect buyers to the hosted page.\n\n## Install & create a session\n\n```\nnpm install @bitpal/checkout\n```\n\n```\nimport { BitPal } from '@bitpal/checkout';\nconst bitpal = new BitPal(process.env.BITPAL_API_KEY); // bp_test_\u2026 or bp_live_\u2026\n\n// A CAIP-2 id is absolute: 'eip155:8453' (Base mainnet) works only with a live key \u2014\n// with a test key it fails with CHAIN_MODE_MISMATCH. Use 'eip155:84532' for test, or a short name.\nconst { data: session } = await bitpal.checkout.createSession({\n line_items: [{ name: 'Pro plan', amount: '29', currency: 'USDC' }],\n pay_chain: 'base', // short name \u2192 Base Sepolia with bp_test_, Base with bp_live_\n expires_in_minutes: 30, // payment window 1\u201360, default 15\n success_url: 'https://your.store/thanks?s={CHECKOUT_SESSION_ID}',\n});\nredirect(session.checkout_url);\n```\n\n## Amounts \u2014 just pass the number\n\nSet `amount` to a plain number (`\"29\"`, `\"0.5\"`) in the line item's `currency`. No decimals to compute.\n\n> **You never compute token decimals.** The amount is quoted in the line item's `currency` \u2014 `USDC`/`USDT` mean USD, `JPYC` means JPY (no FX conversion). It is independent of chain/token decimals: USDC (6dp) on Base or USDT (18dp) on BNB take the same `amount`, and BitPal rescales at deposit time. (Already have 6-decimal integers? Pass `amount_atomic` instead \u2014 the two are mutually exclusive.)\n\n## Example: buyer chooses chain & token\n\n```\nawait bitpal.checkout.createSession({\n line_items: [{ name: 'Order #1024', amount: '49.90', currency: 'USDC' }], // currency = price denomination\n allowed_assets: [ // (chain, token) pairs \u2014 the buyer picks one; mix tokens freely\n { chain: 'base', token: 'USDC' },\n { chain: 'arbitrum', token: 'USDC' },\n { chain: 'tron', token: 'USDT' },\n { chain: 'solana', token: 'USDC' },\n ],\n // shorthand for one token across chains: allowed_pay_chains: ['base','arbitrum','solana']\n});\n```\n\n> **Payment options** `allowed_assets` (chain\xD7token pairs) is the standard way \u2014 the buyer picks any listed pair. `chain` takes a short-name (`'base'`, `'tron'`, `'solana'`) or a CAIP-2 id and resolves to your key's mode (test/live). Shorthands: `allowed_pay_chains` (one token across chains) and `pay_chain` (single fixed chain). `allowed_assets` takes precedence.\n\n## Example: poll status\n\n```\nconst { data } = await bitpal.checkout.getStatus(sessionId);\nif (data.status === 'paid_confirmed' || data.status === 'swept') showSuccess(); // UI only \u2014 fulfil from the webhook\n```\n\nStatus values: `created` \u2192 `awaiting_deposit` \u2192 `deposit_detected` \u2192 `paid_confirmed` \u2192 `swept`, plus `expired`, `unresolved_underpaid
|
|
4126
|
+
markdown: "# SDK\n\nCreate checkout sessions from your backend and redirect buyers to the hosted page.\n\n## Install & create a session\n\n```\nnpm install @bitpal/checkout\n```\n\n```\nimport { BitPal } from '@bitpal/checkout';\nconst bitpal = new BitPal(process.env.BITPAL_API_KEY); // bp_test_\u2026 or bp_live_\u2026\n\n// A CAIP-2 id is absolute: 'eip155:8453' (Base mainnet) works only with a live key \u2014\n// with a test key it fails with CHAIN_MODE_MISMATCH. Use 'eip155:84532' for test, or a short name.\nconst { data: session } = await bitpal.checkout.createSession({\n line_items: [{ name: 'Pro plan', amount: '29', currency: 'USDC' }],\n pay_chain: 'base', // short name \u2192 Base Sepolia with bp_test_, Base with bp_live_\n expires_in_minutes: 30, // payment window 1\u201360, default 15\n success_url: 'https://your.store/thanks?s={CHECKOUT_SESSION_ID}',\n});\nredirect(session.checkout_url);\n```\n\n## Amounts \u2014 just pass the number\n\nSet `amount` to a plain number (`\"29\"`, `\"0.5\"`) in the line item's `currency`. No decimals to compute.\n\n> **You never compute token decimals.** The amount is quoted in the line item's `currency` \u2014 `USDC`/`USDT` mean USD, `JPYC` means JPY (no FX conversion). It is independent of chain/token decimals: USDC (6dp) on Base or USDT (18dp) on BNB take the same `amount`, and BitPal rescales at deposit time. (Already have 6-decimal integers? Pass `amount_atomic` instead \u2014 the two are mutually exclusive.)\n\n## Example: buyer chooses chain & token\n\n```\nawait bitpal.checkout.createSession({\n line_items: [{ name: 'Order #1024', amount: '49.90', currency: 'USDC' }], // currency = price denomination\n allowed_assets: [ // (chain, token) pairs \u2014 the buyer picks one; mix tokens freely\n { chain: 'base', token: 'USDC' },\n { chain: 'arbitrum', token: 'USDC' },\n { chain: 'tron', token: 'USDT' },\n { chain: 'solana', token: 'USDC' },\n ],\n // shorthand for one token across chains: allowed_pay_chains: ['base','arbitrum','solana']\n});\n```\n\n> **Payment options** `allowed_assets` (chain\xD7token pairs) is the standard way \u2014 the buyer picks any listed pair. `chain` takes a short-name (`'base'`, `'tron'`, `'solana'`) or a CAIP-2 id and resolves to your key's mode (test/live). Shorthands: `allowed_pay_chains` (one token across chains) and `pay_chain` (single fixed chain). `allowed_assets` takes precedence.\n\n## Example: poll status\n\n```\nconst { data } = await bitpal.checkout.getStatus(sessionId);\nif (data.status === 'paid_confirmed' || data.status === 'swept') showSuccess(); // UI only \u2014 fulfil from the webhook\n```\n\nStatus values: `created` \u2192 `awaiting_deposit` \u2192 `deposit_detected` \u2192 `paid_confirmed` \u2192 `swept`, plus `expired`, `unresolved_underpaid` (and `wrong_token` on sessions before 2026-11-12). (Sessions opened through the legacy wallet-connect flow may report `pending_payment`; treat it like `created`.) See [Payment lifecycle](/docs/md/lifecycle.md).\n\n## Method reference\n\n| Method | Purpose |\n| --- | --- |\n| `createSession(params)` | Create a session \u2192 `checkout_url`. |\n| `getSession(id)` | Full session (authenticated). |\n| `getPublicSession(id)` | Buyer-facing: payment options, status. |\n| `getStatus(id)` | Lightweight status for polling. |\n| `previewFee(amount)` | Fee preview before charging. |\n| `products` / `prices` | Catalog: reusable products & prices (for payment links). |\n| `paymentLinks` | Create & manage shareable payment links from a price. |\n| `webhooks` | Webhook endpoints: `create`, `list`, `update`, `rotateSecret`, `replay`. With an API key this works **in test mode only** (a `bp_live_` key gets 403 \u2014 manage live endpoints in the console). Verifying deliveries is `verifyWebhookSignature`. |\n| `reports` | Reconciliation: `payments()` / `payouts()` / `fees()` for a date range (JSON pages), or `csv(kind, range)` for the whole range \u2014 see [Payment lifecycle \u2192 Reconciliation](/docs/md/lifecycle.md). |\n\nAmount helpers (advanced, for `amount_atomic`): `toAtomic(v, decimals)` / `fromAtomic(v, decimals)`."
|
|
4127
4127
|
},
|
|
4128
4128
|
{
|
|
4129
4129
|
slug: "webhooks",
|
|
4130
4130
|
title: "Webhooks",
|
|
4131
4131
|
url: "https://bitpal.io/docs/#webhooks",
|
|
4132
4132
|
markdownUrl: "https://bitpal.io/docs/md/webhooks.md",
|
|
4133
|
-
markdown: '# Webhooks\n\nSigned JSON events as a session moves through its lifecycle. Delivered at least once \u2014 keep handlers idempotent.\n\n## Events\n\n| Event | Fires when |\n| --- | --- |\n| `checkout.session.created` | A session is created. |\n| `checkout.session.deposit_detected` | Funds seen on-chain, awaiting confirmations. **Optional** \u2014 skipped when the deposit is already confirmed the first time BitPal sees it (common for a normal payment); `paid` then arrives on its own. Never wait for it. |\n| `checkout.session.paid` | Confirmed \u2014 safe to fulfil. `late: true` if it arrived after the deadline. |\n| `checkout.session.settled` | Funds swept to your payout wallet. |\n| `checkout.session.unresolved` | Underpaid, wrong token, or a confirmed deposit reverted by a reorg (`reason`). |\n| `checkout.session.expired` | Deadline passed without full payment. **Timing:** without a deposit address it arrives at `expires_at` (within about 5 minutes); once a deposit address was issued it arrives **about 90 minutes after `expires_at`**, because a transfer already in flight may still confirm (see [Payment window](/docs/md/lifecycle.md)). |\n| `checkout.refund.recorded` | A refund was made on-chain (`kind`): automatic, or one you sent and recorded in the console (`merchant`). |\n| `x402.payment.resolved` | An x402 call that settled with `503` reached its outcome. **Only fires for those** \u2014 the normal path answers inline. |\n\n## Verify & handle\n\nEach request carries `X-Webhook-Signature-256` (`sha256=<hex>`) and `X-Webhook-Timestamp`. Verify both, against the **raw bytes**.\n\n```\nimport express from \'express\';\nimport { verifyWebhookSignature, parseWebhookEvent, WEBHOOK_HEADERS } from \'@bitpal/checkout\';\n\napp.post(\'/webhooks/bitpal\', express.raw({ type: \'application/json\' }), async (req, res) => {\n const raw = req.body.toString(\'utf8\'); // exact bytes, not re-serialized JSON\n const ok = await verifyWebhookSignature( // returns a Promise \u2014 always await it\n raw,\n req.header(WEBHOOK_HEADERS.signature) ?? \'\',\n process.env.BITPAL_WEBHOOK_SECRET,\n { timestamp: req.header(WEBHOOK_HEADERS.timestamp) ?? \'\' }, // replay defense \u2014 required\n );\n if (!ok) return res.status(400).send(\'invalid signature\');\n const event = parseWebhookEvent(raw);\n if (event.event === \'checkout.session.paid\') await fulfil(event.data.session_id); // idempotent, dedup on event.id\n res.sendStatus(200);\n});\n```\n\n> **Verify against the raw body, and await the result.** Re-serializing the JSON changes the bytes and breaks the signature \u2014 capture the raw body before JSON middleware. `verifyWebhookSignature` is async: without `await` the check is always truthy and forged requests pass.\n\n## Request format\n\nEvery delivery is an HTTP `POST` with a JSON body:\n\n```\n{\n "id": "1ff63374-9417-4f87-850d-e869a3bd2dbe", // event id \u2014 dedup on this\n "event": "checkout.session.paid",\n "created_at": "2026-11-06T07:20:49.549Z", // when the event happened (unchanged on retries)\n "data": { "session_id": "cs_\u2026", \u2026 }\n}\n```\n\n| Header | Value |\n| --- | --- |\n| `X-Webhook-Signature-256` | `sha256=<hex>` \u2014 HMAC-SHA256 of `<X-Webhook-Timestamp>.<raw body>` |\n| `X-Webhook-Timestamp` | ISO-8601 UTC time of **this attempt** (new on every retry) |\n| `X-Webhook-Event` | Event name (unsigned \u2014 read it from the body) |\n| `X-Webhook-Id` | Event id (unsigned \u2014 dedup on the body `id`) |\n| `X-Webhook-Delivery-Id` | Id of this delivery, for support requests |\n\n## Verifying the signature\n\n1. Read the raw request body as bytes, before any JSON parsing.\n2. Reject the request if `X-Webhook-Timestamp` is more than 5 minutes from your clock (replay protection).\n3. Compute `HMAC-SHA256(key = your whsec_\u2026 secret, message = timestamp + "." + raw body)` as lowercase hex.\n4. Compare it with the part after `sha256=` using a constant-time comparison.\n\n### Python\n\n```\nimport hmac, hashlib, time\nfrom datetime import datetime\n\ndef verify_bitpal_webhook(raw_body: bytes, signature: str, timestamp: str, secret: str, tolerance_sec: int = 300) -> bool:\n if not signature.startswith("sha256=") or not timestamp:\n return False\n try:\n sent = datetime.fromisoformat(timestamp.replace("Z", "+00:00")).timestamp()\n except (ValueError, OverflowError, OSError):\n return False\n if abs(time.time() - sent) > tolerance_sec:\n return False\n expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()\n # compare bytes: compare_digest raises TypeError on non-ASCII str input\n return hmac.compare_digest(signature[len("sha256="):].encode(), expected.encode())\n\n# Flask: verify_bitpal_webhook(request.get_data(), request.headers.get("X-Webhook-Signature-256", ""),\n# request.headers.get("X-Webhook-Timestamp", ""), os.environ["BITPAL_WEBHOOK_SECRET"])\n```\n\n### Go\n\n```\nimport (\n "crypto/hmac"\n "crypto/sha256"\n "encoding/hex"\n "strings"\n "time"\n)\n\nfunc VerifyBitPalWebhook(rawBody []byte, signature, timestamp, secret string) bool {\n const prefix = "sha256="\n if !strings.HasPrefix(signature, prefix) || timestamp == "" {\n return false\n }\n sent, err := time.Parse(time.RFC3339Nano, timestamp)\n if err != nil {\n return false\n }\n if d := time.Since(sent); d > 5*time.Minute || d < -5*time.Minute {\n return false\n }\n mac := hmac.New(sha256.New, []byte(secret))\n mac.Write([]byte(timestamp + "."))\n mac.Write(rawBody)\n expected := hex.EncodeToString(mac.Sum(nil))\n return hmac.Equal([]byte(signature[len(prefix):]), []byte(expected))\n}\n```\n\n### PHP\n\n```\nfunction verify_bitpal_webhook(string $rawBody, string $signature, string $timestamp, string $secret, int $toleranceSec = 300): bool {\n if (strpos($signature, \'sha256=\') !== 0 || $timestamp === \'\') return false;\n $sent = strtotime($timestamp);\n if ($sent === false || abs(time() - $sent) > $toleranceSec) return false;\n $expected = hash_hmac(\'sha256\', $timestamp . \'.\' . $rawBody, $secret);\n return hash_equals($expected, substr($signature, 7));\n}\n\n$secret = getenv(\'BITPAL_WEBHOOK_SECRET\'); // false when unset\nif ($secret === false || $secret === \'\') {\n http_response_code(500); // misconfigured \u2014 never verify against an empty key\n exit;\n}\n$ok = verify_bitpal_webhook(\n (string) file_get_contents(\'php://input\'),\n $_SERVER[\'HTTP_X_WEBHOOK_SIGNATURE_256\'] ?? \'\',\n $_SERVER[\'HTTP_X_WEBHOOK_TIMESTAMP\'] ?? \'\',\n $secret\n);\n```\n\n### Java\n\n```\nimport javax.crypto.Mac;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.nio.charset.StandardCharsets;\nimport java.security.MessageDigest;\nimport java.time.DateTimeException;\nimport java.time.Duration;\nimport java.time.Instant;\n\npublic static boolean verifyBitPalWebhook(byte[] rawBody, String signature, String timestamp, String secret) throws Exception {\n if (signature == null || !signature.startsWith("sha256=") || timestamp == null || timestamp.isEmpty()) return false;\n Instant sent;\n try { sent = Instant.parse(timestamp); } catch (DateTimeException e) { return false; }\n if (Duration.between(sent, Instant.now()).abs().getSeconds() > 300) return false;\n Mac mac = Mac.getInstance("HmacSHA256");\n mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));\n mac.update((timestamp + ".").getBytes(StandardCharsets.UTF_8));\n byte[] digest = mac.doFinal(rawBody);\n StringBuilder hex = new StringBuilder();\n for (byte b : digest) hex.append(String.format("%02x", b));\n return MessageDigest.isEqual(hex.toString().getBytes(StandardCharsets.UTF_8),\n signature.substring(7).getBytes(StandardCharsets.UTF_8));\n}\n```\n\n## Event data\n\nEvery `data` object has `session_id` and `metadata` \u2014 the `metadata` you set when creating the session (`{}` if none), so you can map an event to your order without a lookup. BitPal\'s reserved keys (`_bitpal`, `payer_email`, `allowed_pay_chains`, `allowed_assets`) are left out. Session events also carry `amount_total` (6-decimal integer string), `currency` and `late`. Token amounts are in the pay token\'s decimals \u2014 see [Payment lifecycle \u2192 Amounts](/docs/md/lifecycle.md).\n\n| Event | Extra fields in `data` |\n| --- | --- |\n| `checkout.session.created` | \u2014 (only `session_id`, `amount_total`, `currency`) |\n| `checkout.session.deposit_detected` | `tx_hash`, `amount_received`, `received_token` (token contract) \u2014 optional event, see above |\n| `checkout.session.paid` | `tx_hash`, `late` |\n| `checkout.session.settled` | `sweep_tx_hash` |\n| `checkout.session.unresolved` | `reason`: `underpaid` (+ `amount_received`, `amount_expected`, `received_token`, `tx_hash`) \xB7 `wrong_token` (+ `received_token`) \xB7 `reorg` |\n| `checkout.session.expired` | `reason: "no_deposit"`, or `refund_tx_hash` when a partial payment was refunded |\n| `checkout.refund.recorded` | `kind` (`overpay_excess` \xB7 `underpay_expired` \xB7 `wrong_token` \xB7 `late_deposit` \xB7 `merchant`), `amount`, `token`, `tx_hash`, `refund_address`; for `merchant` also `from_address` and `reason_code` |\n\n### Example: `checkout.session.paid`\n\n```\n{\n "id": "1ff63374-9417-4f87-850d-e869a3bd2dbe",\n "event": "checkout.session.paid",\n "created_at": "2026-11-06T07:20:49.549Z",\n "data": {\n "session_id": "cs_HFoj91aFFbHSRk1LN1-9K01q",\n "amount_total": "1000000",\n "currency": "USDC",\n "late": false,\n "tx_hash": "0xd9a75dfe753b20027a0a93841c4e70af1b6be061b59e530d1bc381e88c196fe3",\n "metadata": { "order_id": "ord_1042" }\n }\n}\n```\n\n### Example: `checkout.session.unresolved` (underpaid)\n\n```\n{\n "id": "18d80275-45be-43d7-a57b-4b5038bd2736",\n "event": "checkout.session.unresolved",\n "created_at": "2026-11-06T07:14:19.252Z",\n "data": {\n "session_id": "cs_1ATJ7V5QknzeFpTNPMU_449s",\n "amount_total": "5000000",\n "currency": "USDC",\n "late": false,\n "reason": "underpaid",\n "amount_expected": "5000000",\n "amount_received": "2500000",\n "received_token": "0x943b94Dacf1507F2658f1CfE103Ac36Ebc13Ef65",\n "tx_hash": "0x4461291b5d743d3950a474c015098b8cb53b4373346bd510f74481dd8dbb9d65",\n "metadata": { "order_id": "ord_1041" }\n }\n}\n```\n\n### Example: `checkout.refund.recorded`\n\n```\n{\n "id": "f2fdcce6-1880-44b2-8351-906c42b7c4cd",\n "event": "checkout.refund.recorded",\n "created_at": "2026-11-06T07:20:29.547Z",\n "data": {\n "session_id": "cs_YsrZGcZzVsVHJo8ZNVmsTL1D",\n "kind": "overpay_excess",\n "amount": "1000000",\n "token": "0x943b94Dacf1507F2658f1CfE103Ac36Ebc13Ef65",\n "tx_hash": "0xe943412e2be8b3e3fc6359b47a8eea0cc4de17768b069f52ebab1aacd873aec4",\n "refund_address": "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720",\n "metadata": { "order_id": "ord_1040" }\n }\n}\n```\n\n## Delivery, retries and ordering\n\n- Respond with any **2xx within 10 seconds**. Do slow work after responding (queue it).\n- A failed delivery (non-2xx, timeout or connection error) is retried right away twice (after 1 s and 2 s), then after **1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h** \u2014 10 attempts over about a day. After the last one the delivery moves to the **dead-letter queue**; resend it from Console \u2192 Developers \u2192 Webhooks once your endpoint is fixed (dead-lettered deliveries can be resent for 30 days). You can also resend any past event by id \u2014 see [Resending past events](/docs/md/webhooks.md) below.\n- Delivery is **at least once**: the same event can arrive more than once. Deduplicate on the body `id` and store it in the same transaction as your side effect.\n- Order is not guaranteed \u2014 a retried `paid` can arrive after `settled`. Keep the furthest state you have seen instead of overwriting it with an older one. Not every event is sent for every payment: `paid` can arrive with no `deposit_detected` before it (a late payment, or a deposit already confirmed when it was first seen).\n- Test and live are separate: each environment has its own endpoints and secrets.\n- Only **active** endpoints receive events. While an endpoint is `paused` or `disabled` (removed), new events are not queued for it and are not delivered later if you activate it again, and its pending retries stop \u2014 they move to the dead-letter queue. To catch up, list the events you missed with `GET /v1/checkout/events` and resend them to an active endpoint (see [Resending past events](/docs/md/webhooks.md)).\n\n## Managing endpoints with the API (test mode)\n\nLive endpoints are created and changed only in **Console \u2192 Developers \u2192 Webhooks** \u2014 a leaked live key must not be able to redirect your payment events. In **test mode** you can also manage endpoints with a `bp_test_` API key, which is handy for CI or local development behind a tunnel: `POST /v1/webhooks` (create \u2014 the `whsec_\u2026` secret is in the response, shown only once), `GET /v1/webhooks`, `PATCH /v1/webhooks/{id}` (`status: "disabled"` removes it) and `POST /v1/webhooks/{id}/rotate-secret` (the old secret stops working at once). A `bp_live_` key gets `403`.\n\n- Every change is recorded in your audit log as made by the API key. The console asks for a reason when you remove an endpoint or rotate a secret; with a test key the `reason_code` is optional and is recorded as `api_request` when omitted.\n- The same is available as SDK methods (`bitpal.checkout.webhooks.create / list / update / rotateSecret / replay`) and as MCP tools (`register_test_webhook`, `list_test_webhooks`, `rotate_test_webhook_secret`, `replay_test_event`).\n\n```\ncurl -X POST https://api.bitpal.io/v1/webhooks \\\n -H "Authorization: Bearer $BITPAL_TEST_KEY" -H "Content-Type: application/json" \\\n -d \'{"endpoint_url":"https://abc.ngrok.app/webhooks/bitpal","subscribed_events":["*"]}\'\n# \u2192 { "data": { "id": "\u2026", "secret": "whsec_\u2026", \u2026 } } \u2014 store the secret now\n```\n\n## Resending past events\n\nAn endpoint only receives events that happen after it is registered. To deliver an earlier event \u2014 for example to backfill a new endpoint, or to replay one your handler mishandled \u2014 call `POST /v1/webhooks/{id}/replay` with `{"event_id": "\u2026"}`. `event_id` is the envelope `id` (from a delivery you received, or from the MCP `list_test_events` tool). The event goes out unchanged \u2014 same `id`, so your dedup still applies \u2014 signed with the endpoint\'s current secret and with the usual retries. Only events of the same mode can be resent. With an API key this works in test mode; for live endpoints use the console (failed deliveries can be resent from the dead-letter queue).\n\n## Testing\n\n- **Send Test** (Console \u2192 Developers \u2192 Webhooks) posts a signed sample event to your endpoint. Its id starts with `test_`.\n- In the Sandbox event list (and the MCP `list_test_events` tool) every event is a test-mode event. The `is_test` field does **not** mean test mode: it is `true` only for **Send Test** samples and `false` for events produced by a (simulated) payment.\n- For local development, expose your server with a tunnel (ngrok, cloudflared) and register the tunnel URL in test mode \u2014 in the console, or with your test key (`POST /v1/webhooks`, the SDK or the MCP `register_test_webhook` tool).\n- Run a real test payment ([Quickstart](/docs/md/quickstart.md)) to receive the full sequence of events.'
|
|
4133
|
+
markdown: '# Webhooks\n\nSigned JSON events as a session moves through its lifecycle. Delivered at least once \u2014 keep handlers idempotent.\n\n## Events\n\n| Event | Fires when |\n| --- | --- |\n| `checkout.session.created` | A session is created. |\n| `checkout.session.deposit_detected` | Funds seen on-chain, awaiting confirmations. **Optional** \u2014 skipped when the deposit is already confirmed the first time BitPal sees it (common for a normal payment); `paid` then arrives on its own. Never wait for it. |\n| `checkout.session.paid` | Confirmed \u2014 safe to fulfil. `late: true` if it arrived after the deadline. |\n| `checkout.session.settled` | Funds swept to your payout wallet. |\n| `checkout.session.unresolved` | Underpaid, wrong token, or a confirmed deposit reverted by a reorg (`reason`). |\n| `checkout.session.expired` | Deadline passed without full payment. **Timing:** without a deposit address it arrives at `expires_at` (within about 5 minutes); once a deposit address was issued it arrives **about 90 minutes after `expires_at`**, because a transfer already in flight may still confirm (see [Payment window](/docs/md/lifecycle.md)). |\n| `checkout.refund.recorded` | A refund was made on-chain (`kind`): automatic, or one you sent and recorded in the console (`merchant`). |\n| `x402.payment.resolved` | An x402 call that settled with `503` reached its outcome. **Only fires for those** \u2014 the normal path answers inline. |\n\n## Verify & handle\n\nEach request carries `X-Webhook-Signature-256` (`sha256=<hex>`) and `X-Webhook-Timestamp`. Verify both, against the **raw bytes**.\n\n```\nimport express from \'express\';\nimport { verifyWebhookSignature, parseWebhookEvent, WEBHOOK_HEADERS } from \'@bitpal/checkout\';\n\napp.post(\'/webhooks/bitpal\', express.raw({ type: \'application/json\' }), async (req, res) => {\n const raw = req.body.toString(\'utf8\'); // exact bytes, not re-serialized JSON\n const ok = await verifyWebhookSignature( // returns a Promise \u2014 always await it\n raw,\n req.header(WEBHOOK_HEADERS.signature) ?? \'\',\n process.env.BITPAL_WEBHOOK_SECRET,\n { timestamp: req.header(WEBHOOK_HEADERS.timestamp) ?? \'\' }, // replay defense \u2014 required\n );\n if (!ok) return res.status(400).send(\'invalid signature\');\n const event = parseWebhookEvent(raw);\n if (event.event === \'checkout.session.paid\') await fulfil(event.data.session_id); // idempotent, dedup on event.id\n res.sendStatus(200);\n});\n```\n\n> **Verify against the raw body, and await the result.** Re-serializing the JSON changes the bytes and breaks the signature \u2014 capture the raw body before JSON middleware. `verifyWebhookSignature` is async: without `await` the check is always truthy and forged requests pass.\n\n## Request format\n\nEvery delivery is an HTTP `POST` with a JSON body:\n\n```\n{\n "id": "1ff63374-9417-4f87-850d-e869a3bd2dbe", // event id \u2014 dedup on this\n "event": "checkout.session.paid",\n "created_at": "2026-11-06T07:20:49.549Z", // when the event happened (unchanged on retries)\n "data": { "session_id": "cs_\u2026", \u2026 }\n}\n```\n\n| Header | Value |\n| --- | --- |\n| `X-Webhook-Signature-256` | `sha256=<hex>` \u2014 HMAC-SHA256 of `<X-Webhook-Timestamp>.<raw body>` |\n| `X-Webhook-Timestamp` | ISO-8601 UTC time of **this attempt** (new on every retry) |\n| `X-Webhook-Event` | Event name (unsigned \u2014 read it from the body) |\n| `X-Webhook-Id` | Event id (unsigned \u2014 dedup on the body `id`) |\n| `X-Webhook-Delivery-Id` | Id of this delivery, for support requests |\n\n## Verifying the signature\n\n1. Read the raw request body as bytes, before any JSON parsing.\n2. Reject the request if `X-Webhook-Timestamp` is more than 5 minutes from your clock (replay protection).\n3. Compute `HMAC-SHA256(key = your whsec_\u2026 secret, message = timestamp + "." + raw body)` as lowercase hex.\n4. Compare it with the part after `sha256=` using a constant-time comparison.\n\n### Python\n\n```\nimport hmac, hashlib, time\nfrom datetime import datetime\n\ndef verify_bitpal_webhook(raw_body: bytes, signature: str, timestamp: str, secret: str, tolerance_sec: int = 300) -> bool:\n if not signature.startswith("sha256=") or not timestamp:\n return False\n try:\n sent = datetime.fromisoformat(timestamp.replace("Z", "+00:00")).timestamp()\n except (ValueError, OverflowError, OSError):\n return False\n if abs(time.time() - sent) > tolerance_sec:\n return False\n expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()\n # compare bytes: compare_digest raises TypeError on non-ASCII str input\n return hmac.compare_digest(signature[len("sha256="):].encode(), expected.encode())\n\n# Flask: verify_bitpal_webhook(request.get_data(), request.headers.get("X-Webhook-Signature-256", ""),\n# request.headers.get("X-Webhook-Timestamp", ""), os.environ["BITPAL_WEBHOOK_SECRET"])\n```\n\n### Go\n\n```\nimport (\n "crypto/hmac"\n "crypto/sha256"\n "encoding/hex"\n "strings"\n "time"\n)\n\nfunc VerifyBitPalWebhook(rawBody []byte, signature, timestamp, secret string) bool {\n const prefix = "sha256="\n if !strings.HasPrefix(signature, prefix) || timestamp == "" {\n return false\n }\n sent, err := time.Parse(time.RFC3339Nano, timestamp)\n if err != nil {\n return false\n }\n if d := time.Since(sent); d > 5*time.Minute || d < -5*time.Minute {\n return false\n }\n mac := hmac.New(sha256.New, []byte(secret))\n mac.Write([]byte(timestamp + "."))\n mac.Write(rawBody)\n expected := hex.EncodeToString(mac.Sum(nil))\n return hmac.Equal([]byte(signature[len(prefix):]), []byte(expected))\n}\n```\n\n### PHP\n\n```\nfunction verify_bitpal_webhook(string $rawBody, string $signature, string $timestamp, string $secret, int $toleranceSec = 300): bool {\n if (strpos($signature, \'sha256=\') !== 0 || $timestamp === \'\') return false;\n $sent = strtotime($timestamp);\n if ($sent === false || abs(time() - $sent) > $toleranceSec) return false;\n $expected = hash_hmac(\'sha256\', $timestamp . \'.\' . $rawBody, $secret);\n return hash_equals($expected, substr($signature, 7));\n}\n\n$secret = getenv(\'BITPAL_WEBHOOK_SECRET\'); // false when unset\nif ($secret === false || $secret === \'\') {\n http_response_code(500); // misconfigured \u2014 never verify against an empty key\n exit;\n}\n$ok = verify_bitpal_webhook(\n (string) file_get_contents(\'php://input\'),\n $_SERVER[\'HTTP_X_WEBHOOK_SIGNATURE_256\'] ?? \'\',\n $_SERVER[\'HTTP_X_WEBHOOK_TIMESTAMP\'] ?? \'\',\n $secret\n);\n```\n\n### Java\n\n```\nimport javax.crypto.Mac;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.nio.charset.StandardCharsets;\nimport java.security.MessageDigest;\nimport java.time.DateTimeException;\nimport java.time.Duration;\nimport java.time.Instant;\n\npublic static boolean verifyBitPalWebhook(byte[] rawBody, String signature, String timestamp, String secret) throws Exception {\n if (signature == null || !signature.startsWith("sha256=") || timestamp == null || timestamp.isEmpty()) return false;\n Instant sent;\n try { sent = Instant.parse(timestamp); } catch (DateTimeException e) { return false; }\n if (Duration.between(sent, Instant.now()).abs().getSeconds() > 300) return false;\n Mac mac = Mac.getInstance("HmacSHA256");\n mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));\n mac.update((timestamp + ".").getBytes(StandardCharsets.UTF_8));\n byte[] digest = mac.doFinal(rawBody);\n StringBuilder hex = new StringBuilder();\n for (byte b : digest) hex.append(String.format("%02x", b));\n return MessageDigest.isEqual(hex.toString().getBytes(StandardCharsets.UTF_8),\n signature.substring(7).getBytes(StandardCharsets.UTF_8));\n}\n```\n\n## Event data\n\nEvery `data` object has `session_id` and `metadata` \u2014 the `metadata` you set when creating the session (`{}` if none), so you can map an event to your order without a lookup. BitPal\'s reserved keys (`_bitpal`, `payer_email`, `allowed_pay_chains`, `allowed_assets`) are left out. Session events also carry `amount_total` (6-decimal integer string), `currency` and `late`. Deposit events (`deposit_detected`, `paid`, `unresolved`, `settled`, `expired`) also carry `deposit_tx_hash`, `received_token_symbol` and `token_decimals` \u2014 the same names as on the session (`tx_hash` is still sent with the same value). Token amounts are in the token\'s own decimals (`token_decimals`; 18 on BNB Chain USDC/USDT and JPYC) \u2014 see [Payment lifecycle \u2192 Amounts](/docs/md/lifecycle.md).\n\n| Event | Extra fields in `data` |\n| --- | --- |\n| `checkout.session.created` | \u2014 (only `session_id`, `amount_total`, `currency`) |\n| `checkout.session.deposit_detected` | `tx_hash`, `amount_received`, `received_token` (token contract) \u2014 optional event, see above |\n| `checkout.session.paid` | `deposit_tx_hash` (= `tx_hash`), `received_token_symbol`, `token_decimals`, `late` |\n| `checkout.session.settled` | `sweep_tx_hash` |\n| `checkout.session.unresolved` | `reason`: `underpaid` (+ `amount_received`, `amount_expected`, `received_token`, `deposit_tx_hash`) \xB7 `late` (a full payment arrived after the deadline on a `late_payments: "refund"` session \u2014 not accepted, refunded automatically at `refund_available_at`; sent once; + `amount_received`, `received_token`, `deposit_tx_hash`, `refund_available_at`) \xB7 `reorg` |\n| `checkout.session.expired` | `reason` (`no_deposit` \xB7 `underpaid` \xB7 `late_payment_refunded`), `expired_at`, `source` on every expiry; `refund_tx_hash` when what was received was refunded |\n| `checkout.refund.recorded` | `kind` (`overpay_excess` \xB7 `underpay_expired` \xB7 `wrong_token` \xB7 `late_deposit` \xB7 `merchant`), `amount` (in `token_decimals`), `token`, `token_symbol`, `token_decimals`, `tx_hash`, `refund_address`; for `merchant` also `from_address` and `reason_code` |\n| Legacy: sessions created before 2026-11-12 may also send `checkout.session.unresolved` with `reason: "wrong_token"`. A wrong token now arrives as `checkout.refund.recorded` with `kind: "wrong_token"` and the session stays `awaiting_deposit`. |\n\n### Example: `checkout.session.paid`\n\n```\n{\n "id": "1ff63374-9417-4f87-850d-e869a3bd2dbe",\n "event": "checkout.session.paid",\n "created_at": "2026-11-06T07:20:49.549Z",\n "data": {\n "session_id": "cs_HFoj91aFFbHSRk1LN1-9K01q",\n "amount_total": "1000000",\n "currency": "USDC",\n "late": false,\n "tx_hash": "0xd9a75dfe753b20027a0a93841c4e70af1b6be061b59e530d1bc381e88c196fe3",\n "metadata": { "order_id": "ord_1042" }\n }\n}\n```\n\n### Example: `checkout.session.unresolved` (underpaid)\n\n```\n{\n "id": "18d80275-45be-43d7-a57b-4b5038bd2736",\n "event": "checkout.session.unresolved",\n "created_at": "2026-11-06T07:14:19.252Z",\n "data": {\n "session_id": "cs_1ATJ7V5QknzeFpTNPMU_449s",\n "amount_total": "5000000",\n "currency": "USDC",\n "late": false,\n "reason": "underpaid",\n "amount_expected": "5000000",\n "amount_received": "2500000",\n "received_token": "0x943b94Dacf1507F2658f1CfE103Ac36Ebc13Ef65",\n "tx_hash": "0x4461291b5d743d3950a474c015098b8cb53b4373346bd510f74481dd8dbb9d65",\n "metadata": { "order_id": "ord_1041" }\n }\n}\n```\n\n### Example: `checkout.refund.recorded`\n\n```\n{\n "id": "f2fdcce6-1880-44b2-8351-906c42b7c4cd",\n "event": "checkout.refund.recorded",\n "created_at": "2026-11-06T07:20:29.547Z",\n "data": {\n "session_id": "cs_YsrZGcZzVsVHJo8ZNVmsTL1D",\n "kind": "overpay_excess",\n "amount": "1000000",\n "token": "0x943b94Dacf1507F2658f1CfE103Ac36Ebc13Ef65",\n "tx_hash": "0xe943412e2be8b3e3fc6359b47a8eea0cc4de17768b069f52ebab1aacd873aec4",\n "refund_address": "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720",\n "metadata": { "order_id": "ord_1040" }\n }\n}\n```\n\n## Delivery, retries and ordering\n\n- Respond with any **2xx within 10 seconds**. Do slow work after responding (queue it).\n- A failed delivery (non-2xx, timeout or connection error) is retried right away twice (after 1 s and 2 s), then after **1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h** \u2014 10 attempts over about a day. After the last one the delivery moves to the **dead-letter queue**; resend it from Console \u2192 Developers \u2192 Webhooks once your endpoint is fixed (dead-lettered deliveries can be resent for 30 days). You can also resend any past event by id \u2014 see [Resending past events](/docs/md/webhooks.md) below.\n- Delivery is **at least once**: the same event can arrive more than once. Deduplicate on the body `id` and store it in the same transaction as your side effect.\n- Order is not guaranteed \u2014 a retried `paid` can arrive after `settled`. Keep the furthest state you have seen instead of overwriting it with an older one. Not every event is sent for every payment: `paid` can arrive with no `deposit_detected` before it (a late payment, or a deposit already confirmed when it was first seen).\n- Test and live are separate: each environment has its own endpoints and secrets.\n- Only **active** endpoints receive events. While an endpoint is `paused` or `disabled` (removed), new events are not queued for it and are not delivered later if you activate it again, and its pending retries stop \u2014 they move to the dead-letter queue. To catch up, list the events you missed with `GET /v1/checkout/events` and resend them to an active endpoint (see [Resending past events](/docs/md/webhooks.md)).\n\n## Managing endpoints with the API (test mode)\n\nLive endpoints are created and changed only in **Console \u2192 Developers \u2192 Webhooks** \u2014 a leaked live key must not be able to redirect your payment events. In **test mode** you can also manage endpoints with a `bp_test_` API key, which is handy for CI or local development behind a tunnel: `POST /v1/webhooks` (create \u2014 the `whsec_\u2026` secret is in the response, shown only once), `GET /v1/webhooks`, `PATCH /v1/webhooks/{id}` (`status: "disabled"` removes it) and `POST /v1/webhooks/{id}/rotate-secret` (the old secret stops working at once). A `bp_live_` key gets `403`.\n\n- Every change is recorded in your audit log as made by the API key. The console asks for a reason when you remove an endpoint or rotate a secret; with a test key the `reason_code` is optional and is recorded as `api_request` when omitted.\n- The same is available as SDK methods (`bitpal.checkout.webhooks.create / list / update / rotateSecret / replay`) and as MCP tools (`register_test_webhook`, `list_test_webhooks`, `rotate_test_webhook_secret`, `replay_test_event`).\n\n```\ncurl -X POST https://api.bitpal.io/v1/webhooks \\\n -H "Authorization: Bearer $BITPAL_TEST_KEY" -H "Content-Type: application/json" \\\n -d \'{"endpoint_url":"https://abc.ngrok.app/webhooks/bitpal","subscribed_events":["*"]}\'\n# \u2192 { "data": { "id": "\u2026", "secret": "whsec_\u2026", \u2026 } } \u2014 store the secret now\n```\n\n## Resending past events\n\nAn endpoint only receives events that happen after it is registered. To deliver an earlier event \u2014 for example to backfill a new endpoint, or to replay one your handler mishandled \u2014 call `POST /v1/webhooks/{id}/replay` with `{"event_id": "\u2026"}`. `event_id` is the envelope `id` (from a delivery you received, or from the MCP `list_test_events` tool). The event goes out unchanged \u2014 same `id`, so your dedup still applies \u2014 signed with the endpoint\'s current secret and with the usual retries. Only events of the same mode can be resent. With an API key this works in test mode; for live endpoints use the console (failed deliveries can be resent from the dead-letter queue).\n\n## Testing\n\n- **Send Test** (Console \u2192 Developers \u2192 Webhooks) posts a signed sample event to your endpoint. Its id starts with `test_`.\n- In the Sandbox event list (and the MCP `list_test_events` tool) every event is a test-mode event. The `is_test` field does **not** mean test mode: it is `true` only for **Send Test** samples and `false` for events produced by a (simulated) payment.\n- For local development, expose your server with a tunnel (ngrok, cloudflared) and register the tunnel URL in test mode \u2014 in the console, or with your test key (`POST /v1/webhooks`, the SDK or the MCP `register_test_webhook` tool).\n- Run a real test payment ([Quickstart](/docs/md/quickstart.md)) to receive the full sequence of events.'
|
|
4134
4134
|
},
|
|
4135
4135
|
{
|
|
4136
4136
|
slug: "x402",
|
|
@@ -4251,14 +4251,14 @@ var docs_data_default = {
|
|
|
4251
4251
|
pageTitle: "API reference",
|
|
4252
4252
|
heading: "Checkout sessions \u203A `GET /v1/checkout/sessions` \u2014 List checkout sessions",
|
|
4253
4253
|
url: "https://bitpal.io/docs/#api",
|
|
4254
|
-
text: '### `GET /v1/checkout/sessions` \u2014 List checkout sessions\n\nNewest first, in the API key\u2019s mode (test/live).\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `status` | query | string | No | Filter by status, or `succeeded` for all successful states. |\n| `limit` | query | integer | No | 1\u2013100, default `50` |\n| `offset` | query | integer | No | \u2265 0, default `0` |\n\n**Response `200`** \u2014 Sessions.\n\n```\n{\n "data": [\n {\n "id": "string",\n "status": "string",\n "mode": "TEST",\n "amount_total": "1000000",\n "amount_received": "string",\n "currency": "string",\n "pay_chain": "string",\n "pay_asset": "string",\n "token_decimals": 0,\n "payment_method": "string",\n "payer_wallet": "string",\n "seller_wallet": "string",\n "tolerance_percent": "string",\n "late_payments": "accept",\n "deposit_address": "string",\n "refund_address": "string",\n "received_token": "string",\n "received_token_symbol": "string",\n "fee_amount": "string",\n "fee_rate": "string",\n "net_amount": "string",\n "fee_amount_token": "string",\n "net_amount_token": "string",\n "tx_hash": "string",\n "deposit_tx_hash": "string",\n "sweep_tx_hash": "string",\n "success_url": "string",\n "cancel_url": "string",\n "metadata": {},\n "payment_source": "string",\n "payment_link_id": "string",\n "expires_at": "2026-11-06T07:00:00.000Z",\n "paid_at": "2026-11-06T07:00:00.000Z",\n "settled_at": "2026-11-06T07:00:00.000Z",\n "created_at": "2026-11-06T07:00:00.000Z",\n "raw_status": "string"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \u2014 see [Errors](/docs/md/errors.md).'
|
|
4254
|
+
text: '### `GET /v1/checkout/sessions` \u2014 List checkout sessions\n\nNewest first, in the API key\u2019s mode (test/live).\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `status` | query | string | No | Filter by status, or `succeeded` for all successful states. |\n| `limit` | query | integer | No | 1\u2013100, default `50` |\n| `offset` | query | integer | No | \u2265 0, default `0` |\n\n**Response `200`** \u2014 Sessions.\n\n```\n{\n "data": [\n {\n "id": "string",\n "status": "string",\n "mode": "TEST",\n "amount_total": "1000000",\n "amount_received": "string",\n "currency": "string",\n "pay_chain": "string",\n "pay_asset": "string",\n "token_decimals": 0,\n "payment_method": "string",\n "payer_wallet": "string",\n "seller_wallet": "string",\n "tolerance_percent": "string",\n "late_payments": "accept",\n "deposit_address": "string",\n "refund_address": "string",\n "received_token": "string",\n "received_token_symbol": "string",\n "fee_amount": "string",\n "fee_rate": "string",\n "net_amount": "string",\n "fee_amount_token": "string",\n "net_amount_token": "string",\n "tx_hash": "string",\n "deposit_tx_hash": "string",\n "sweep_tx_hash": "string",\n "refund_status": "none",\n "success_url": "string",\n "cancel_url": "string",\n "metadata": {},\n "payment_source": "string",\n "payment_link_id": "string",\n "expires_at": "2026-11-06T07:00:00.000Z",\n "paid_at": "2026-11-06T07:00:00.000Z",\n "settled_at": "2026-11-06T07:00:00.000Z",\n "created_at": "2026-11-06T07:00:00.000Z",\n "raw_status": "string"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \u2014 see [Errors](/docs/md/errors.md).'
|
|
4255
4255
|
},
|
|
4256
4256
|
{
|
|
4257
4257
|
page: "api",
|
|
4258
4258
|
pageTitle: "API reference",
|
|
4259
4259
|
heading: "Checkout sessions \u203A `GET /v1/checkout/sessions/{id}` \u2014 Retrieve a checkout session",
|
|
4260
4260
|
url: "https://bitpal.io/docs/#api",
|
|
4261
|
-
text: '### `GET /v1/checkout/sessions/{id}` \u2014 Retrieve a checkout session\n\nServer-side source of truth for an order, together with webhooks.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Checkout session id (`cs_\u2026`). |\n\n**Response `200`** \u2014 The session.\n\n```\n{\n "data": {\n "id": "string",\n "status": "string",\n "mode": "TEST",\n "amount_total": "1000000",\n "amount_received": "string",\n "currency": "string",\n "pay_chain": "string",\n "pay_asset": "string",\n "token_decimals": 0,\n "payment_method": "string",\n "payer_wallet": "string",\n "seller_wallet": "string",\n "tolerance_percent": "string",\n "late_payments": "accept",\n "deposit_address": "string",\n "refund_address": "string",\n "received_token": "string",\n "received_token_symbol": "string",\n "fee_amount": "string",\n "fee_rate": "string",\n "net_amount": "string",\n "fee_amount_token": "string",\n "net_amount_token": "string",\n "tx_hash": "string",\n "deposit_tx_hash": "string",\n "sweep_tx_hash": "string",\n "success_url": "string",\n "cancel_url": "string",\n "metadata": {},\n "payment_source": "string",\n "payment_link_id": "string",\n "expires_at": "2026-11-06T07:00:00.000Z",\n "paid_at": "2026-11-06T07:00:00.000Z",\n "settled_at": "2026-11-06T07:00:00.000Z",\n "created_at": "2026-11-06T07:00:00.000Z",\n "line_items": [\n {\n "id": "string",\n "name": "string",\n "amount": "1000000",\n "currency": "string",\n "quantity": 0,\n "price_id": "string",\n "product_id": "string",\n "created_at": "2026-11-06T07:00:00.000Z"\n }\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).'
|
|
4261
|
+
text: '### `GET /v1/checkout/sessions/{id}` \u2014 Retrieve a checkout session\n\nServer-side source of truth for an order, together with webhooks.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Checkout session id (`cs_\u2026`). |\n\n**Response `200`** \u2014 The session.\n\n```\n{\n "data": {\n "id": "string",\n "status": "string",\n "mode": "TEST",\n "amount_total": "1000000",\n "amount_received": "string",\n "currency": "string",\n "pay_chain": "string",\n "pay_asset": "string",\n "token_decimals": 0,\n "payment_method": "string",\n "payer_wallet": "string",\n "seller_wallet": "string",\n "tolerance_percent": "string",\n "late_payments": "accept",\n "deposit_address": "string",\n "refund_address": "string",\n "received_token": "string",\n "received_token_symbol": "string",\n "fee_amount": "string",\n "fee_rate": "string",\n "net_amount": "string",\n "fee_amount_token": "string",\n "net_amount_token": "string",\n "tx_hash": "string",\n "deposit_tx_hash": "string",\n "sweep_tx_hash": "string",\n "refund_status": "none",\n "success_url": "string",\n "cancel_url": "string",\n "metadata": {},\n "payment_source": "string",\n "payment_link_id": "string",\n "expires_at": "2026-11-06T07:00:00.000Z",\n "paid_at": "2026-11-06T07:00:00.000Z",\n "settled_at": "2026-11-06T07:00:00.000Z",\n "created_at": "2026-11-06T07:00:00.000Z",\n "line_items": [\n {\n "id": "string",\n "name": "string",\n "amount": "1000000",\n "currency": "string",\n "quantity": 0,\n "price_id": "string",\n "product_id": "string",\n "created_at": "2026-11-06T07:00:00.000Z"\n }\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).'
|
|
4262
4262
|
},
|
|
4263
4263
|
{
|
|
4264
4264
|
page: "api",
|
|
@@ -4393,6 +4393,41 @@ var docs_data_default = {
|
|
|
4393
4393
|
url: "https://bitpal.io/docs/#api",
|
|
4394
4394
|
text: '### `PATCH /v1/checkout/payment-links/{id}` \u2014 Update a payment link\n\nOperational settings and active \u2194 inactive. The price cannot change \u2014 create a new link instead. Deactivating (`status: "inactive"`) accepts an optional `reason_code` (`user_request`, `security_concern`, `rotation`, `no_longer_needed` or `other`, plus an optional `reason_note`); with an API key, a request without it is recorded as `api_request` (the console always asks for a reason). Reactivating accepts the same fields optionally. Every status change is recorded in your audit log with its reason.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Payment link id (`plink_\u2026`). |\n\n**Request body** (`UpdatePaymentLinkRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `title` | string \\| null | No | 1\u2013200 chars |\n| `description` | string \\| null | No | \u2264 2000 chars |\n| `collect_email` | boolean | No | |\n| `email_required` | boolean | No | |\n| `allowed_pay_chains` | array of string \\| null | No | 1\u20138 items |\n| `allowed_assets` | array of object \\| null | No | 1\u201316 items |\n| `success_url` | string (uri) \\| null | No | \u2264 2048 chars |\n| `cancel_url` | string (uri) \\| null | No | \u2264 2048 chars |\n| `status` | "active" \\| "inactive" | No | |\n| `max_uses` | integer \\| null | No | \u2265 1 |\n| `expires_at` | string (date-time) \\| null | No | |\n| `metadata` | map | No | |\n| `reason_code` | "user_request" \\| "security_concern" \\| "rotation" \\| "no_longer_needed" \\| "other" | No | |\n| `reason_note` | string | No | \u2264 500 chars |\n\n**Response `200`** \u2014 The updated link.\n\n```\n{\n "data": {\n "id": "string",\n "share_token": "string",\n "price_id": "string",\n "title": "string",\n "description": "string",\n "collect_email": true,\n "email_required": true,\n "allowed_pay_chains": [\n "string"\n ],\n "allowed_assets": [\n {\n "caip2": "string",\n "token": "string"\n }\n ],\n "success_url": "string",\n "cancel_url": "string",\n "status": "active",\n "max_uses": 0,\n "paid_use_count": 0,\n "expires_at": "2026-11-06T07:00:00.000Z",\n "metadata": {},\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `404` \xB7 `409` \u2014 see [Errors](/docs/md/errors.md).'
|
|
4395
4395
|
},
|
|
4396
|
+
{
|
|
4397
|
+
page: "api",
|
|
4398
|
+
pageTitle: "API reference",
|
|
4399
|
+
heading: "Customers",
|
|
4400
|
+
url: "https://bitpal.io/docs/#api",
|
|
4401
|
+
text: "## Customers\n\nBuyers who paid you (created automatically from paid sessions) and their recent payments. Scoped to the current mode."
|
|
4402
|
+
},
|
|
4403
|
+
{
|
|
4404
|
+
page: "api",
|
|
4405
|
+
pageTitle: "API reference",
|
|
4406
|
+
heading: "Customers \u203A `POST /v1/checkout/customers` \u2014 Create a customer",
|
|
4407
|
+
url: "https://bitpal.io/docs/#api",
|
|
4408
|
+
text: '### `POST /v1/checkout/customers` \u2014 Create a customer\n\nCustomers are also created automatically from paid sessions (by payer wallet, or by email when there is no wallet). A wallet address can exist once per mode \u2014 a duplicate returns 409 `CUSTOMER_ALREADY_EXISTS`.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `Idempotency-Key` | header | string | No | Accepted but not yet enforced on this endpoint \u2014 retries are not de-duplicated by key. (8\u2013128 chars) |\n\n**Request body** (`CreateCustomerRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `wallet_address` | string | Yes | Payer wallet (0x\u2026 / T\u2026 / base58). Unique per account and mode. (1\u2013128 chars) |\n| `email` | string (email) \\| null | No | \u2264 320 chars |\n| `name` | string \\| null | No | \u2264 200 chars |\n| `phone` | string \\| null | No | \u2264 50 chars |\n| `country` | string \\| null | No | Free text; an ISO 3166-1 alpha-2 code such as "US" is recommended. (\u2264 100 chars) |\n| `metadata` | map | No | |\n\n```\n{\n "wallet_address": "0x1111111111111111111111111111111111111111",\n "email": "buyer@example.com"\n}\n```\n\n**Response `201`** \u2014 Customer created.\n\n```\n{\n "data": {\n "id": "string",\n "wallet_address": "string",\n "email": "string",\n "name": "string",\n "phone": "string",\n "country": "string",\n "metadata": {},\n "status": "active",\n "total_spent": "1000000",\n "transaction_count": 0,\n "first_payment_at": "2026-11-06T07:00:00.000Z",\n "last_payment_at": "2026-11-06T07:00:00.000Z",\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `409` \u2014 see [Errors](/docs/md/errors.md).'
|
|
4409
|
+
},
|
|
4410
|
+
{
|
|
4411
|
+
page: "api",
|
|
4412
|
+
pageTitle: "API reference",
|
|
4413
|
+
heading: "Customers \u203A `GET /v1/checkout/customers` \u2014 List customers",
|
|
4414
|
+
url: "https://bitpal.io/docs/#api",
|
|
4415
|
+
text: '### `GET /v1/checkout/customers` \u2014 List customers\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `limit` | query | integer | No | 1\u2013100, default `20` |\n| `offset` | query | integer | No | \u2265 0 |\n| `page` | query | integer | No | \u2265 1 |\n| `search` | query | string | No | 1\u2013200 chars |\n| `sort` | query | "created_at" \\| "email" \\| "name" \\| "wallet_address" \\| "total_spent" \\| "transaction_count" | No | default `"created_at"` |\n| `order` | query | "asc" \\| "desc" | No | default `"desc"` |\n\n**Response `200`** \u2014 Customers in the current mode, with `meta.total` for paging.\n\n```\n{\n "data": [\n {\n "id": "string",\n "wallet_address": "string",\n "email": "string",\n "name": "string",\n "phone": "string",\n "country": "string",\n "metadata": {},\n "status": "active",\n "total_spent": "1000000",\n "transaction_count": 0,\n "first_payment_at": "2026-11-06T07:00:00.000Z",\n "last_payment_at": "2026-11-06T07:00:00.000Z",\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n }\n ],\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \u2014 see [Errors](/docs/md/errors.md).'
|
|
4416
|
+
},
|
|
4417
|
+
{
|
|
4418
|
+
page: "api",
|
|
4419
|
+
pageTitle: "API reference",
|
|
4420
|
+
heading: "Customers \u203A `GET /v1/checkout/customers/{id}` \u2014 Retrieve a customer",
|
|
4421
|
+
url: "https://bitpal.io/docs/#api",
|
|
4422
|
+
text: '### `GET /v1/checkout/customers/{id}` \u2014 Retrieve a customer\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Customer id (uuid). |\n\n**Response `200`** \u2014 The customer and their recent payments.\n\n```\n{\n "data": {\n "customer": {\n "id": "string",\n "wallet_address": "string",\n "email": "string",\n "name": "string",\n "phone": "string",\n "country": "string",\n "metadata": {},\n "status": "active",\n "total_spent": "1000000",\n "transaction_count": 0,\n "first_payment_at": "2026-11-06T07:00:00.000Z",\n "last_payment_at": "2026-11-06T07:00:00.000Z",\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "recent_payments": [\n {}\n ]\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).'
|
|
4423
|
+
},
|
|
4424
|
+
{
|
|
4425
|
+
page: "api",
|
|
4426
|
+
pageTitle: "API reference",
|
|
4427
|
+
heading: "Customers \u203A `PATCH /v1/checkout/customers/{id}` \u2014 Update a customer",
|
|
4428
|
+
url: "https://bitpal.io/docs/#api",
|
|
4429
|
+
text: '### `PATCH /v1/checkout/customers/{id}` \u2014 Update a customer\n\nOnly the fields you send change. Send `null` to clear a field. The wallet address cannot change.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Customer id (uuid). |\n\n**Request body** (`UpdateCustomerRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `email` | string (email) \\| null | No | \u2264 320 chars |\n| `name` | string \\| null | No | \u2264 200 chars |\n| `phone` | string \\| null | No | \u2264 50 chars |\n| `country` | string \\| null | No | \u2264 100 chars |\n| `metadata` | map | No | |\n\n**Response `200`** \u2014 The updated customer.\n\n```\n{\n "data": {\n "id": "string",\n "wallet_address": "string",\n "email": "string",\n "name": "string",\n "phone": "string",\n "country": "string",\n "metadata": {},\n "status": "active",\n "total_spent": "1000000",\n "transaction_count": 0,\n "first_payment_at": "2026-11-06T07:00:00.000Z",\n "last_payment_at": "2026-11-06T07:00:00.000Z",\n "mode": "TEST",\n "created_at": "2026-11-06T07:00:00.000Z",\n "updated_at": "2026-11-06T07:00:00.000Z"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `404` \u2014 see [Errors](/docs/md/errors.md).'
|
|
4430
|
+
},
|
|
4396
4431
|
{
|
|
4397
4432
|
page: "api",
|
|
4398
4433
|
pageTitle: "API reference",
|
|
@@ -4510,7 +4545,7 @@ var docs_data_default = {
|
|
|
4510
4545
|
pageTitle: "API reference",
|
|
4511
4546
|
heading: "Sandbox \u203A `POST /v1/sandbox/sessions/{id}/simulate` \u2014 Simulate a buyer payment",
|
|
4512
4547
|
url: "https://bitpal.io/docs/#api",
|
|
4513
|
-
text: '### `POST /v1/sandbox/sessions/{id}/simulate` \u2014 Simulate a buyer payment\n\nTest mode only: BitPal sends a testnet deposit to the session for you, so you can test fulfilment and webhooks without a wallet. Scenarios: `exact` (pays the rest \u2014 the session gets paid), `underpay` (half of the rest), `overpay` (the rest + 1 token \u2014 the excess is refunded), `wrong_token` (1 unit of another token on the same chain \u2014 returned; the session stays payable). Issues the deposit address first if needed. A `bp_live_` key gets 403 `SANDBOX_TEST_MODE_ONLY`. Limited to 10 calls per minute and 300 per day per account.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Checkout session id (`cs_\u2026`). |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`SimulateSandboxPaymentRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `scenario` | "exact" \\| "underpay" \\| "overpay" \\| "wrong_token" | Yes | `exact`, `underpay`, `overpay` or `wrong_token`. |\n| `chain` | string | No | Only before the deposit address is issued, for a session with several payment options: the network to pay on (with `token`). (\u2265 1 chars) |\n| `token` | string | No | Goes with `chain`, e.g. `USDC`. (\u2265 1 chars) |\n| `refund_address` | string | No | Buyer refund address, used only when this call issues the deposit address. Omit it and refunds go to BitPal\u2019s test buyer wallet. (1\u2013128 chars) |\n\n```\n{\n "scenario": "exact"\n}\n```\n\n**Response `201`** \u2014 The deposit was sent.\n\n```\n{\n "data": {\n "session_id": "string",\n "scenario": "exact",\n "chain": "string",\n "token": "string",\n "amount": "1000000",\n "decimals": 0,\n "deposit_address": "string",\n "tx_hash": "string"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `404` \xB7 `409` \xB7 `429` \u2014 see [Errors](/docs/md/errors.md).'
|
|
4548
|
+
text: '### `POST /v1/sandbox/sessions/{id}/simulate` \u2014 Simulate a buyer payment\n\nTest mode only: BitPal sends a testnet deposit to the session for you, so you can test fulfilment and webhooks without a wallet. Scenarios: `exact` (pays the rest \u2014 the session gets paid), `underpay` (half of the rest), `overpay` (the rest + 1 token \u2014 the excess is refunded), `wrong_token` (1 unit of another token on the same chain \u2014 returned; the session stays payable). Issues the deposit address first if needed (a scenario the chain cannot run is rejected before an address is issued). `wrong_token` needs a second test token on the chain \u2014 today BNB Smart Chain Testnet and Polygon Amoy; elsewhere 400 `SANDBOX_SCENARIO_UNAVAILABLE` lists the chains that work (`details.supported_chains`). **Late payments:** on a session whose deposit address was issued and that then expired unpaid (`expires_at` + 90 min), `exact`/`overpay`/`underpay` send a payment after the deadline \u2014 `late_payments: "accept"` \u2192 `checkout.session.paid` with `late: true`; `"refund"` \u2192 `checkout.session.unresolved` with `reason: "late"` and an automatic refund about 7 days after the deadline. A session that expired without an address cannot be paid (409 `SANDBOX_SESSION_NOT_PAYABLE`). A `bp_live_` key gets 403 `SANDBOX_TEST_MODE_ONLY`. Limited to 10 calls per minute and 300 per day per account.\n\n**Auth:** API key \u2014 `Authorization: Bearer bp_test_\u2026` / `bp_live_\u2026` (server only)\n\n**Parameters**\n\n| Parameter | In | Type | Required | Description |\n| --- | --- | --- | --- | --- |\n| `id` | path | string | Yes | Checkout session id (`cs_\u2026`). |\n| `Idempotency-Key` | header | string | No | 8\u2013128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with `X-Idempotency-Replayed: true`. (8\u2013128 chars) |\n\n**Request body** (`SimulateSandboxPaymentRequest`)\n\n| Field | Type | Required | Description |\n| --- | --- | --- | --- |\n| `scenario` | "exact" \\| "underpay" \\| "overpay" \\| "wrong_token" | Yes | `exact`, `underpay`, `overpay` or `wrong_token`. |\n| `chain` | string | No | Only before the deposit address is issued, for a session with several payment options: the network to pay on (with `token`). (\u2265 1 chars) |\n| `token` | string | No | Goes with `chain`, e.g. `USDC`. (\u2265 1 chars) |\n| `refund_address` | string | No | Buyer refund address, used only when this call issues the deposit address. Omit it and refunds go to BitPal\u2019s test buyer wallet. (1\u2013128 chars) |\n\n```\n{\n "scenario": "exact"\n}\n```\n\n**Response `201`** \u2014 The deposit was sent.\n\n```\n{\n "data": {\n "session_id": "string",\n "scenario": "exact",\n "chain": "string",\n "token": "string",\n "amount": "1000000",\n "decimals": 0,\n "deposit_address": "string",\n "tx_hash": "string"\n },\n "meta": {\n "mode": "TEST",\n "requestId": "string",\n "total": 0,\n "limit": 0,\n "offset": 0\n }\n}\n```\n\n**Errors:** `400` \xB7 `401` \xB7 `403` \xB7 `404` \xB7 `409` \xB7 `429` \u2014 see [Errors](/docs/md/errors.md).'
|
|
4514
4549
|
},
|
|
4515
4550
|
{
|
|
4516
4551
|
page: "api",
|
|
@@ -4580,7 +4615,7 @@ var docs_data_default = {
|
|
|
4580
4615
|
pageTitle: "API reference",
|
|
4581
4616
|
heading: "Webhook events",
|
|
4582
4617
|
url: "https://bitpal.io/docs/#api",
|
|
4583
|
-
text: '## Webhook events\n\nEnvelope: `{ id, event, created_at, data }`, signed with `X-Webhook-Signature-256`. Verification and retries: [Webhooks](/docs/md/webhooks.md).\n\n| Event | Fires when | `data` fields |\n| --- | --- | --- |\n| `checkout.session.created` | A session is created. | `session_id`, `metadata`, `amount_total`, `currency`, `late` |\n| `checkout.session.deposit_detected` | Funds seen on-chain, awaiting confirmations. Optional \u2014 skipped when the first time BitPal sees the deposit it is already confirmed; then `checkout.session.paid` (or `unresolved`) arrives with no `deposit_detected` before it. Do not wait for it. | `session_id`, `metadata`, `amount_total`, `currency`, `late`, `tx_hash`, `amount_received`, `received_token` |\n| `checkout.session.paid` | Confirmed \u2014 safe to fulfil. `late: true` if it arrived after the deadline. | `session_id`, `metadata`, `amount_total`, `currency`, `late`, `tx_hash` |\n| `checkout.session.settled` | Funds swept to your payout wallet. | `session_id`, `metadata`, `amount_total`, `currency`, `late`, `sweep_tx_hash` |\n| `checkout.session.unresolved` |
|
|
4618
|
+
text: '## Webhook events\n\nEnvelope: `{ id, event, created_at, data }`, signed with `X-Webhook-Signature-256`. Verification and retries: [Webhooks](/docs/md/webhooks.md).\n\n| Event | Fires when | `data` fields |\n| --- | --- | --- |\n| `checkout.session.created` | A session is created. | `session_id`, `metadata`, `amount_total`, `currency`, `late` |\n| `checkout.session.deposit_detected` | Funds seen on-chain, awaiting confirmations. Optional \u2014 skipped when the first time BitPal sees the deposit it is already confirmed; then `checkout.session.paid` (or `unresolved`) arrives with no `deposit_detected` before it. Do not wait for it. | `session_id`, `metadata`, `amount_total`, `currency`, `late`, `deposit_tx_hash`, `received_token_symbol`, `token_decimals`, `tx_hash`, `amount_received`, `received_token` |\n| `checkout.session.paid` | Confirmed \u2014 safe to fulfil. `late: true` if it arrived after the deadline. | `session_id`, `metadata`, `amount_total`, `currency`, `late`, `deposit_tx_hash`, `received_token_symbol`, `token_decimals`, `tx_hash` |\n| `checkout.session.settled` | Funds swept to your payout wallet. | `session_id`, `metadata`, `amount_total`, `currency`, `late`, `deposit_tx_hash`, `received_token_symbol`, `token_decimals`, `sweep_tx_hash` |\n| `checkout.session.unresolved` | Needs attention (`reason`): `underpaid`, `late` (a full payment arrived after the deadline on a session created with `late_payments: "refund"` \u2014 it is not accepted and is refunded automatically at `refund_available_at`; sent once), or `reorg` (a confirmed deposit was reverted). `wrong_token` only on sessions created before 2026-11-12. | `session_id`, `metadata`, `amount_total`, `currency`, `late`, `deposit_tx_hash`, `received_token_symbol`, `token_decimals`, `reason`, `amount_expected`, `amount_received`, `received_token`, `tx_hash`, `refund_available_at` |\n| `checkout.session.expired` | Deadline passed without full payment. Timing: a session that never got a deposit address expires at `expires_at` (applied within about 5 minutes, or at once when the session is read). Once a deposit address was issued, this event is sent shortly after the 90-minute grace that follows `expires_at` ends (`expires_at` + 90 min), because a transfer already in flight may still confirm. If you cannot hold stock that long, release it yourself at `expires_at`; a full payment can still arrive later as `checkout.session.paid` with `late: true` (unless the session was created with `late_payments: "refund"`). | `session_id`, `metadata`, `amount_total`, `currency`, `late`, `deposit_tx_hash`, `received_token_symbol`, `token_decimals`, `reason`, `expired_at`, `source`, `refund_tx_hash` |\n| `checkout.refund.recorded` | A refund was made on-chain (`kind`): automatic, or recorded by you in the console (`merchant`). | `session_id`, `metadata`, `kind`, `amount`, `token`, `token_symbol`, `token_decimals`, `tx_hash`, `refund_address`, `from_address`, `reason_code` |\n| `x402.payment.resolved` | An x402 call that settled with `503` reached its outcome. Only fires for those \u2014 the normal path answers inline. | `payment_id`, `status`, `resource`, `amount`, `asset`, `chain`, `tx_hash`, `payer`, `resolved_by` |'
|
|
4584
4619
|
},
|
|
4585
4620
|
{
|
|
4586
4621
|
page: "errors",
|
|
@@ -4636,14 +4671,14 @@ var docs_data_default = {
|
|
|
4636
4671
|
pageTitle: "Popup & headless",
|
|
4637
4672
|
heading: "Popup",
|
|
4638
4673
|
url: "https://bitpal.io/docs/#flows",
|
|
4639
|
-
text: '## Popup\n\nBrowsers block `window.open` unless it runs directly in the click handler. Open an empty window first, then point it at the checkout URL once your backend has created the session.\n\n```\nasync function onPayClick() {\n // 1) open synchronously, inside the click handler\n const popup = window.open(\'\', \'bitpal_checkout\', \'width=460,height=720\');\n\n // 2) your backend creates the session (secret key stays on the server)\n const { checkout_url, session_id } = await fetch(\'/api/checkout\', { method: \'POST\' }).then((r) => r.json());\n\n // 3) navigate the popup \u2014 or fall back to a full redirect if it was blocked\n if (popup && !popup.closed) popup.location.href = checkout_url;\n else window.location.href = checkout_url;\n\n const origin = new URL(checkout_url).origin;\n window.addEventListener(\'message\', function onMessage(e) {\n if (e.origin !== origin) return; // trust only the checkout page\n const m = e.data;\n if (!m || m.source !== \'bitpal-checkout\' || m.type !== \'terminal\') return;\n window.removeEventListener(\'message\', onMessage);\n refreshOrderFromServer(session_id); // m.step: done | expired | error\n });\n}\n```\n\nWhen the payment reaches a final screen, the checkout page posts one message to its opener:\n\n```\n{ "source": "bitpal-checkout", "type": "terminal", "step": "done", "session_id": "cs_\u2026" }\n```\n\n- `step` is `done`, `expired` or `error`. One `terminal` message is sent per final screen.\n- If the buyer sends the wrong token, the page posts `{ "type": "status", "step": "wrong_token" }` instead. It is not final: the wrong token is returned
|
|
4674
|
+
text: '## Popup\n\nBrowsers block `window.open` unless it runs directly in the click handler. Open an empty window first, then point it at the checkout URL once your backend has created the session.\n\n```\nasync function onPayClick() {\n // 1) open synchronously, inside the click handler\n const popup = window.open(\'\', \'bitpal_checkout\', \'width=460,height=720\');\n\n // 2) your backend creates the session (secret key stays on the server)\n const { checkout_url, session_id } = await fetch(\'/api/checkout\', { method: \'POST\' }).then((r) => r.json());\n\n // 3) navigate the popup \u2014 or fall back to a full redirect if it was blocked\n if (popup && !popup.closed) popup.location.href = checkout_url;\n else window.location.href = checkout_url;\n\n const origin = new URL(checkout_url).origin;\n window.addEventListener(\'message\', function onMessage(e) {\n if (e.origin !== origin) return; // trust only the checkout page\n const m = e.data;\n if (!m || m.source !== \'bitpal-checkout\' || m.type !== \'terminal\') return;\n window.removeEventListener(\'message\', onMessage);\n refreshOrderFromServer(session_id); // m.step: done | expired | error\n });\n}\n```\n\nWhen the payment reaches a final screen, the checkout page posts one message to its opener:\n\n```\n{ "source": "bitpal-checkout", "type": "terminal", "step": "done", "session_id": "cs_\u2026" }\n```\n\n- `step` is `done`, `expired` or `error`. One `terminal` message is sent per final screen.\n- If the buyer sends the wrong token, the page posts `{ "type": "status", "step": "wrong_token" }` instead. It is not final: the wrong token is returned, the session stays `awaiting_deposit` and can still be paid with the right one, so keep listening for `terminal`.\n- The message is a **UI hint**. Re-read the order from your server (which learned the result from the webhook) before showing success.\n- Messages can be lost (the buyer closes the window, browser privacy settings). Keep a fallback: poll your own backend for the order status.\n- Don\'t close the popup yourself \u2014 the completion screen lets the buyer email a receipt. The popup closes itself only after the buyer sends that receipt; otherwise the buyer closes it with the **Close** button, so don\'t wait for the window to close.'
|
|
4640
4675
|
},
|
|
4641
4676
|
{
|
|
4642
4677
|
page: "flows",
|
|
4643
4678
|
pageTitle: "Popup & headless",
|
|
4644
4679
|
heading: "Headless (your own UI)",
|
|
4645
4680
|
url: "https://bitpal.io/docs/#flows",
|
|
4646
|
-
text: '## Headless (your own UI)\n\nUse this when you render the payment screen yourself. The buyer still pays to a per-order deposit address; you just display it.\n\n> **Call BitPal from your backend.** Browsers on your domain may only call the public session endpoint directly (CORS). Issue the deposit address and poll status through your server, and never ship a secret key to the browser.\n\n1. **Create the session** on your server (as in the Quickstart). Keep `id` and `session_token`.\n2. **Show the payment options.** `getPublicSession(id)` returns `payment_options` \u2014 one entry per allowed (chain, token) with `caip2`, `token_symbol`, `decimals` and the exact `amount_atomic`.\n3. **Collect a refund address** from the buyer, in the format of the chosen network (EVM `0x\u2026`, Tron `T\u2026`, Solana base58). It is committed into the deposit address and cannot change.\n4. **Issue the deposit address** (backend):\n\n ```\n const { data } = await bitpal.checkout.issueDepositAddress(session.id, {\n session_token: session.session_token,\n refundAddress: \'0xBuyerRefundWallet\u2026\',\n chain: \'eip155:84532\',\n token: \'USDC\',\n });\n // {\n // "sessionId": "cs_\u2026",\n // "chain": "eip155:84532",\n // "depositAddress": "0x6b19e871c934aB52775B6712e110f3e4b0626347",\n // "expectedToken": "0x943b94dacf1507f2658f1cfe103ac36ebc13ef65",\n // "expectedAmount": "29000000", // in the token\'s own decimals\n // "feeAmount": "435000",\n // "refundAddress": "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720",\n // "deadline": "1793957308", // on-chain deadline (unix s) = expiresAt + 5400 (90-minute grace)\n // "expiresAt": "2026-11-06T07:58:28.776Z"\n // }\n ```\n5. **Display** the address, a QR code and the exact amount \u2014 format `expectedAmount` with the token\'s decimals (6 for USDC, 18 for BNB USDT and JPYC). Show the countdown to `expiresAt`.\n6. **Track status** through your backend (`getStatus(id)`) for the UI, and **fulfil from the webhook**.\n\nCalling `issueDepositAddress` again for the same session returns the same address **and keeps the first refund address** \u2014 a later call cannot change it. A deposit address belongs to one order; don\'t reuse it.\n\n> **Tell the buyer to send only the selected token, on the selected network.** BitPal returns automatically only the stablecoins it supports on that network. Any other token is not returned automatically, and the network\'s **native coin** (ETH, BNB, POL, TRX, SOL \u2026) sent to a deposit address **cannot be recovered by BitPal**. Show this next to the address.'
|
|
4681
|
+
text: '## Headless (your own UI)\n\nUse this when you render the payment screen yourself. The buyer still pays to a per-order deposit address; you just display it.\n\n> **Call BitPal from your backend.** Browsers on your domain may only call the public session endpoint directly (CORS). Issue the deposit address and poll status through your server, and never ship a secret key to the browser.\n\n1. **Create the session** on your server (as in the Quickstart). Keep `id` and `session_token`.\n2. **Show the payment options.** `getPublicSession(id)` returns `payment_options` \u2014 one entry per allowed (chain, token) with `caip2`, `token_symbol`, `decimals` and the exact `amount_atomic`.\n3. **Collect a refund address** from the buyer, in the format of the chosen network (EVM `0x\u2026`, Tron `T\u2026`, Solana base58). It is committed into the deposit address and cannot change.\n4. **Issue the deposit address** (backend):\n\n ```\n const { data } = await bitpal.checkout.issueDepositAddress(session.id, {\n session_token: session.session_token,\n refundAddress: \'0xBuyerRefundWallet\u2026\',\n chain: \'eip155:84532\',\n token: \'USDC\',\n });\n // {\n // "sessionId": "cs_\u2026",\n // "chain": "eip155:84532",\n // "depositAddress": "0x6b19e871c934aB52775B6712e110f3e4b0626347",\n // "expectedToken": "0x943b94dacf1507f2658f1cfe103ac36ebc13ef65",\n // "expectedAmount": "29000000", // in the token\'s own decimals\n // "feeAmount": "435000",\n // "refundAddress": "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720",\n // "deadline": "1793957308", // on-chain deadline (unix s) = expiresAt + 5400 (90-minute grace)\n // "expiresAt": "2026-11-06T07:58:28.776Z"\n // }\n ```\n5. **Display** the address, a QR code and the exact amount \u2014 format `expectedAmount` with the token\'s decimals (6 for most USDC/USDT, 18 for BNB Chain USDT/USDC and JPYC). Show the countdown to `expiresAt`.\n6. **Track status** through your backend (`getStatus(id)`) for the UI, and **fulfil from the webhook**.\n\nCalling `issueDepositAddress` again for the same session returns the same address **and keeps the first refund address** \u2014 a later call cannot change it. A deposit address belongs to one order; don\'t reuse it.\n\n> **Tell the buyer to send only the selected token, on the selected network.** BitPal returns automatically only the stablecoins it supports on that network. Any other token is not returned automatically, and the network\'s **native coin** (ETH, BNB, POL, TRX, SOL \u2026) sent to a deposit address **cannot be recovered by BitPal**. Show this next to the address.'
|
|
4647
4682
|
},
|
|
4648
4683
|
{
|
|
4649
4684
|
page: "lifecycle",
|
|
@@ -4657,14 +4692,14 @@ var docs_data_default = {
|
|
|
4657
4692
|
pageTitle: "Payment lifecycle",
|
|
4658
4693
|
heading: "States",
|
|
4659
4694
|
url: "https://bitpal.io/docs/#lifecycle",
|
|
4660
|
-
text: '## States\n\n```\ncreated \u2500\u2500\u25B6 awaiting_deposit \u2500\u2500\u25B6 deposit_detected \u2500\u2500\u25B6 paid_confirmed \u2500\u2500\u25B6 swept\n \u2502
|
|
4695
|
+
text: '## States\n\n```\ncreated \u2500\u2500\u25B6 awaiting_deposit \u2500\u2500\u25B6 deposit_detected \u2500\u2500\u25B6 paid_confirmed \u2500\u2500\u25B6 swept\n \u2502 \u2502 \u2502\n \u2502 \u2514\u2500\u25B6 unresolved_underpaid \u2500\u2500(deadline)\u2500\u2500\u25B6 expired (partial refund)\n \u2514\u2500\u2500(deadline, nothing paid)\u2500\u2500\u25B6 expired\n```\n\n| Status | Meaning | What you do |\n| --- | --- | --- |\n| `created` | Session exists; the buyer has not picked a payment option yet. | Nothing. |\n| `awaiting_deposit` | A deposit address was issued; waiting for funds. | Nothing. |\n| `deposit_detected` | Funds seen on-chain, waiting for confirmations. | Show "processing". **Don\'t fulfil yet.** |\n| `paid_confirmed` | The full amount is confirmed. | **Fulfil** (webhook `checkout.session.paid`). |\n| `swept` | Funds moved to your payout wallet, minus the fee. | Reconcile (webhook `checkout.session.settled`). |\n| `unresolved_underpaid` | Less than the amount arrived. The buyer can top up until the deadline. | Wait. If it is still short at the deadline, it is refunded and the session expires. |\n| `wrong_token` | Legacy: sessions before 2026-11-12 that received another stablecoin BitPal supports on that network. Still payable with the right token. New sessions stay `awaiting_deposit` instead \u2014 the returned token shows up as a `wrong_token` refund (see Refunds). | Nothing. |\n| `expired` | The payment window closed without a full payment. | Release the order. A full payment can still arrive later \u2014 see Late payments. |\n\n> **Chain reorganisations.** Rarely, a confirmed deposit disappears after a chain reorg. You then receive `checkout.session.unresolved` with `reason: "reorg"` and the session goes back to `awaiting_deposit` (or `expired` past the deadline). If you have already fulfilled, treat it like a reversed payment.'
|
|
4661
4696
|
},
|
|
4662
4697
|
{
|
|
4663
4698
|
page: "lifecycle",
|
|
4664
4699
|
pageTitle: "Payment lifecycle",
|
|
4665
4700
|
heading: "Amounts",
|
|
4666
4701
|
url: "https://bitpal.io/docs/#lifecycle",
|
|
4667
|
-
text: '## Amounts\n\n| Field | Unit |\n| --- | --- |\n| `line_items[].amount` (input) | Plain number string in the line item\'s `currency`: `"29"`, `"0.5"`. `USDC`/`USDT` = USD, `JPYC` = JPY. No FX conversion. |\n| `amount_atomic` (input), `amount_total` | Always a **6-decimal integer string**, whatever token the buyer pays with: `"29000000"` = 29. |\n| `expectedAmount`, `pay_amount`, `amount_received`, webhook `amount_expected`/`amount_received`, refund `amount` | Integer string in the **pay token\'s own decimals** (`token_decimals`: 6 for USDC/USDT on most chains, 18 for BNB USDT and JPYC). |\n| `fee_amount`, `net_amount` | 6-decimal integer strings; `fee_rate` is a decimal fraction (`"0.015"` = 1.5%). |\n\nHandle all amounts as strings or big integers \u2014 never as floating-point numbers.\n\n**Minimum amount.** A session\'s total (sum of `amount \xD7 quantity`) must be at least **1.00 in the session currency** (USDC/USDT: $1, JPYC: \xA51) \u2014 a lower total returns `400 CHECKOUT_AMOUNT_TOO_LOW`. The schema can\'t express this (it is a rule on the sum, and `amount` alone may be `"0"`), so check it before calling. Per-network minimums come on top: Tron currently $300 (testnet included) and JPYC \xA5100 on every network. A network whose minimum is above the total is not offered to the buyer; requesting a deposit address on it returns `400 CHECKOUT_BELOW_MIN_PAYMENT`.'
|
|
4702
|
+
text: '## Amounts\n\n| Field | Unit |\n| --- | --- |\n| `line_items[].amount` (input) | Plain number string in the line item\'s `currency`: `"29"`, `"0.5"`. `USDC`/`USDT` = USD, `JPYC` = JPY. No FX conversion. |\n| `amount_atomic` (input), `amount_total` | Always a **6-decimal integer string**, whatever token the buyer pays with: `"29000000"` = 29. |\n| `expectedAmount`, `pay_amount`, `amount_received`, webhook `amount_expected`/`amount_received`, refund `amount` | Integer string in the **pay token\'s own decimals** (`token_decimals`: 6 for USDC/USDT on most chains, 18 for BNB Chain USDT/USDC and JPYC). |\n| `fee_amount`, `net_amount` | 6-decimal integer strings; `fee_rate` is a decimal fraction (`"0.015"` = 1.5%). |\n\nHandle all amounts as strings or big integers \u2014 never as floating-point numbers.\n\n**Minimum amount.** A session\'s total (sum of `amount \xD7 quantity`) must be at least **1.00 in the session currency** (USDC/USDT: $1, JPYC: \xA51) \u2014 a lower total returns `400 CHECKOUT_AMOUNT_TOO_LOW`. The schema can\'t express this (it is a rule on the sum, and `amount` alone may be `"0"`), so check it before calling. Per-network minimums come on top: Tron currently $300 (testnet included) and JPYC \xA5100 on every network. A network whose minimum is above the total is not offered to the buyer; requesting a deposit address on it returns `400 CHECKOUT_BELOW_MIN_PAYMENT`.'
|
|
4668
4703
|
},
|
|
4669
4704
|
{
|
|
4670
4705
|
page: "lifecycle",
|
|
@@ -4685,7 +4720,7 @@ var docs_data_default = {
|
|
|
4685
4720
|
pageTitle: "Payment lifecycle",
|
|
4686
4721
|
heading: "Payment window and late payments",
|
|
4687
4722
|
url: "https://bitpal.io/docs/#lifecycle",
|
|
4688
|
-
text: '## Payment window and late payments\n\n- The buyer has `expires_in_minutes` (1\u201360, default 15) from session creation to pay. That deadline is the session\'s `expires_at`.\n- `late` compares the **block time of the deposit transaction** with `expires_at`. A transfer sent in time is `"late": false` even if its confirmations finish after the deadline; `checkout.session.settled` carries the same value as `paid`.\n- Once a deposit address has been issued, BitPal keeps the session open for **90 minutes after `expires_at`** so transfers already in flight can confirm. Underpayment refunds and `checkout.session.expired` for an issued session happen at the end of that window \u2014 so for such a session the `expired` event arrives about 90 minutes after `expires_at`, not at `expires_at`.\n- A session that never got a deposit address (the buyer never picked a network) expires at `expires_at`: BitPal marks it expired within about 5 minutes, or immediately when the session is read, and sends `checkout.session.expired` with `reason: "no_deposit"` then.\n- **Holding stock?** If you cannot keep an item reserved for the extra 90 minutes, release it yourself at `expires_at` instead of waiting for the event. A full payment that still arrives is then reported as `checkout.session.paid` with `"late": true` (or refunded, with `"late_payments": "refund"`), so be ready to fulfil or refund it.\n- **A full payment that arrives after the deadline is accepted as a late payment** for up to 7 days by default: you get `checkout.session.paid` with `"late": true` and the funds settle normally. Decide whether to fulfil, or refund it from the console (see Refunds below).\n- **Don\'t want late payments?** Create the session with `"late_payments": "refund"`. A full payment that arrives after the deadline (`expires_at` + 90 minutes) is then never accepted: it stays in the deposit address and is refunded automatically to the buyer\'s refund address **about 7 days after the deadline** (refund kind `late_deposit`, `checkout.refund.recorded`). The refund can\'t be instant \u2014 BitPal is non-custodial and the deposit contract only lets anyone but you take the funds back after that 7-day window. A transfer sent before `expires_at` whose confirmations finish later is still accepted.\n- This only applies to a session with no earlier on-chain action. If a wrong token was already returned or an underpayment already refunded, the expired session is closed for good: later deposits to its address are not accepted as payment (contact support with the session id and transaction hash).\n- Acceptance ends 1 hour before the on-chain refund window opens (`expires_at` + 90 minutes + 7 days). A full payment that arrives after that cutoff is not accepted; it is refunded automatically once the window opens (refund kind `late_deposit`).\n- A **partial** payment after the deadline is refunded automatically (refund kind `late_deposit`).\n- Funds sent more than 7 days after the deadline are not watched automatically. Contact support with the session id and transaction hash.'
|
|
4723
|
+
text: '## Payment window and late payments\n\n- The buyer has `expires_in_minutes` (1\u201360, default 15) from session creation to pay. That deadline is the session\'s `expires_at`.\n- `late` compares the **block time of the deposit transaction** with `expires_at`. A transfer sent in time is `"late": false` even if its confirmations finish after the deadline; `checkout.session.settled` carries the same value as `paid`.\n- Once a deposit address has been issued, BitPal keeps the session open for **90 minutes after `expires_at`** so transfers already in flight can confirm. Underpayment refunds and `checkout.session.expired` for an issued session happen at the end of that window \u2014 so for such a session the `expired` event arrives about 90 minutes after `expires_at`, not at `expires_at`.\n- A session that never got a deposit address (the buyer never picked a network) expires at `expires_at`: BitPal marks it expired within about 5 minutes, or immediately when the session is read, and sends `checkout.session.expired` with `reason: "no_deposit"` then.\n- **Holding stock?** If you cannot keep an item reserved for the extra 90 minutes, release it yourself at `expires_at` instead of waiting for the event. A full payment that still arrives is then reported as `checkout.session.paid` with `"late": true` (or refunded, with `"late_payments": "refund"`), so be ready to fulfil or refund it.\n- **A full payment that arrives after the deadline is accepted as a late payment** for up to 7 days by default: you get `checkout.session.paid` with `"late": true` and the funds settle normally. Decide whether to fulfil, or refund it from the console (see Refunds below).\n- **Don\'t want late payments?** Create the session with `"late_payments": "refund"`. A full payment that arrives after the deadline (`expires_at` + 90 minutes) is then never accepted: it stays in the deposit address and is refunded automatically to the buyer\'s refund address **about 7 days after the deadline** (refund kind `late_deposit`, `checkout.refund.recorded`). The refund can\'t be instant \u2014 BitPal is non-custodial and the deposit contract only lets anyone but you take the funds back after that 7-day window. As soon as such a payment is seen you get `checkout.session.unresolved` with `reason: "late"` and `refund_available_at` (once), so you can tell the buyer. A transfer sent before `expires_at` whose confirmations finish later is still accepted.\n- This only applies to a session with no earlier on-chain action. If a wrong token was already returned or an underpayment already refunded, the expired session is closed for good: later deposits to its address are not accepted as payment (contact support with the session id and transaction hash).\n- Acceptance ends 1 hour before the on-chain refund window opens (`expires_at` + 90 minutes + 7 days). A full payment that arrives after that cutoff is not accepted; it is refunded automatically once the window opens (refund kind `late_deposit`).\n- A **partial** payment after the deadline is refunded automatically (refund kind `late_deposit`).\n- Funds sent more than 7 days after the deadline are not watched automatically. Contact support with the session id and transaction hash.'
|
|
4689
4724
|
},
|
|
4690
4725
|
{
|
|
4691
4726
|
page: "lifecycle",
|
|
@@ -4706,7 +4741,7 @@ var docs_data_default = {
|
|
|
4706
4741
|
pageTitle: "Payment lifecycle",
|
|
4707
4742
|
heading: "Refunds \u203A 2. Refunding a completed payment (from the console)",
|
|
4708
4743
|
url: "https://bitpal.io/docs/#lifecycle",
|
|
4709
|
-
text: "### 2. Refunding a completed payment (from the console)\n\nLike an app store, a completed payment is refunded by you, in the BitPal console:\n\n1. Open **Console \u2192 Sessions \u2192 the session \u2192 Refund**. It shows the network, token, the buyer's refund address (entered at payment time) and how much is still refundable (received minus refunds so far). Partial refunds are fine.\n2. **Send the refund from your own wallet** \u2014
|
|
4744
|
+
text: "### 2. Refunding a completed payment (from the console)\n\nLike an app store, a completed payment is refunded by you, in the BitPal console:\n\n1. Open **Console \u2192 Sessions \u2192 the session \u2192 Refund**. It shows the network, token, the buyer's refund address (entered at payment time) and how much is still refundable (received minus refunds so far). Partial refunds are fine.\n2. **Send the refund from your own wallet** \u2014 usually the payout wallet the payment settled to. One click with a connected EVM wallet (**Connect wallet** links MetaMask and others even if you signed in with Google; BitPal checks the balance first), or copy the address, amount and token contract (with a QR code) for Tron, Solana or any other wallet.\n3. **Record the transaction hash.** BitPal checks it on-chain (confirmed, successful, the session's token sent to the refund address, after the payment, not more than what is left) and records it. You get `checkout.refund.recorded` with `kind: \"merchant\"`, the `reason_code` you picked and `from_address`; `GET /v1/checkout/sessions/{id}/refunds` lists it.\n\nAfter a refund the session `status` stays `swept` (settled) \u2014 the payment itself happened. Read `refund_status` on the session (`none` \xB7 `partially_refunded` \xB7 `refunded`; only refunds you recorded count) \u2014 the console shows it as a badge next to the status.\n\nCurrent policy: you pay the network fee and the refund amount; the BitPal fee on the original payment is not returned. Recording is console-only \u2014 an API key gets `403 REFUND_CONSOLE_ONLY`. Recording the same transaction again returns the existing record.\n\nTreat `kind` as open: new values may be added. Handle unknown kinds as a generic refund."
|
|
4710
4745
|
},
|
|
4711
4746
|
{
|
|
4712
4747
|
page: "lifecycle",
|
|
@@ -4755,7 +4790,7 @@ var docs_data_default = {
|
|
|
4755
4790
|
pageTitle: "Accept crypto. Get paid directly.",
|
|
4756
4791
|
heading: "What you can accept",
|
|
4757
4792
|
url: "https://bitpal.io/docs/#overview",
|
|
4758
|
-
text: "## What you can accept\n\n| Token | Networks |\n| --- | --- |\n| **USDC** | Base \xB7 Arbitrum \xB7 Solana |\n| **USDT** |
|
|
4793
|
+
text: "## What you can accept\n\n| Token | Networks |\n| --- | --- |\n| **USDC** | Base \xB7 Arbitrum \xB7 Optimism \xB7 BNB (BSC) \xB7 Polygon \xB7 Solana |\n| **USDT** | BNB (BSC) \xB7 Polygon \xB7 Solana \xB7 Tron (coming soon) |\n| **JPYC** | Polygon \xB7 Kaia |\n\nSame-chain, same-token \u2014 no bridging or swapping. Full identifiers & decimals in [Chains & contracts](/docs/md/reference.md)."
|
|
4759
4794
|
},
|
|
4760
4795
|
{
|
|
4761
4796
|
page: "overview",
|
|
@@ -4797,7 +4832,7 @@ var docs_data_default = {
|
|
|
4797
4832
|
pageTitle: "Quickstart",
|
|
4798
4833
|
heading: "2. Send the buyer to the checkout page",
|
|
4799
4834
|
url: "https://bitpal.io/docs/#quickstart",
|
|
4800
|
-
text: "## 2. Send the buyer to the checkout page\n\nRedirect to `checkout_url`, or open it in a popup so the buyer stays on your page (see [Popup & headless](/docs/md/flows.md)). On the page the buyer:\n\n1. picks a network and token (if you allowed more than one),\n2. enters a **refund address** \u2014 over/under-payments are returned there,\n3. gets a one-time **deposit address** and the exact amount, and sends it from any wallet or exchange.\n\nIn test mode the page shows **Get test assets**: it mints test tokens to the buyer's wallet so you can complete a real on-chain payment.\n\n> **No testnet wallet?** Console \u2192 Developers \u2192 **Sandbox** creates a session and sends the deposit for you \u2014 pay in full, underpay, overpay or send the wrong token \u2014 and shows every webhook event as your endpoint receives it. Test mode only; 10 simulations per minute and 300 per day per account."
|
|
4835
|
+
text: "## 2. Send the buyer to the checkout page\n\nRedirect to `checkout_url`, or open it in a popup so the buyer stays on your page (see [Popup & headless](/docs/md/flows.md)). On the page the buyer:\n\n1. picks a network and token (if you allowed more than one),\n2. enters a **refund address** \u2014 over/under-payments are returned there,\n3. gets a one-time **deposit address** and the exact amount, and sends it from any wallet or exchange.\n\nIn test mode the page shows **Get test assets**: it mints test tokens to the buyer's wallet so you can complete a real on-chain payment.\n\n> **No testnet wallet?** Console \u2192 Developers \u2192 **Sandbox** creates a session and sends the deposit for you \u2014 pay in full, underpay, overpay or send the wrong token (BNB Smart Chain Testnet and Polygon Amoy, which have a second test token) \u2014 and shows every webhook event as your endpoint receives it. To test a late payment, let a session with an issued deposit address expire, then simulate again. Test mode only; 10 simulations per minute and 300 per day per account."
|
|
4801
4836
|
},
|
|
4802
4837
|
{
|
|
4803
4838
|
page: "quickstart",
|
|
@@ -4832,21 +4867,21 @@ var docs_data_default = {
|
|
|
4832
4867
|
pageTitle: "Chains & contracts",
|
|
4833
4868
|
heading: "Chains & identifiers",
|
|
4834
4869
|
url: "https://bitpal.io/docs/#reference",
|
|
4835
|
-
text: "## Chains & identifiers\n\nThe `pay_chain` / `allowed_pay_chains` values are [CAIP-2](https://chainagnostic.org/CAIPs/caip-2) identifiers. Use the value from the column matching your API key's mode.\n\n| Network | Tokens | Decimals | `pay_chain` \u2014 mainnet | `pay_chain` \u2014 testnet |\n| --- | --- | --- | --- | --- |\n| **Base** | USDC | 6 | `eip155:8453` | `eip155:84532` |\n| **Arbitrum** | USDC | 6 | `eip155:42161` | `eip155:421614` |\n| **BNB Smart Chain** | USDT | 18 | `eip155:56` | `eip155:97` |\n| **Tron** | USDT | 6 | `tron:0x2b6653dc` | `tron:0xcd8690dc` |\n| **Solana** | USDC \xB7 USDT | 6 | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` |\n| **Polygon** | JPYC | 18 | `eip155:137` | `eip155:80002` |\n| **Kaia** | JPYC | 18 | `eip155:8217` | `eip155:1001` |\n\n> JPYC (Polygon/Kaia) settles 1:1 to JPY \u2014 no FX conversion.\n\n> Test keys (`bp_test_\u2026`) accept only testnet values; live keys only mainnet."
|
|
4870
|
+
text: "## Chains & identifiers\n\nThe `pay_chain` / `allowed_pay_chains` values are [CAIP-2](https://chainagnostic.org/CAIPs/caip-2) identifiers. Use the value from the column matching your API key's mode.\n\n| Network | Tokens | Decimals | `pay_chain` \u2014 mainnet | `pay_chain` \u2014 testnet |\n| --- | --- | --- | --- | --- |\n| **Base** | USDC | 6 | `eip155:8453` | `eip155:84532` |\n| **Arbitrum** | USDC | 6 | `eip155:42161` | `eip155:421614` |\n| **Optimism** | USDC | 6 | `eip155:10` | `eip155:11155420` |\n| **BNB Smart Chain** | USDT \xB7 USDC | 18 \xB7 18 | `eip155:56` | `eip155:97` |\n| **Tron** | USDT | 6 | `tron:0x2b6653dc` (coming soon) | `tron:0xcd8690dc` |\n| **Solana** | USDC \xB7 USDT | 6 | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` |\n| **Polygon** | JPYC \xB7 USDC \xB7 USDT | 18 \xB7 6 \xB7 6 | `eip155:137` | `eip155:80002` |\n| **Kaia** | JPYC | 18 | `eip155:8217` | `eip155:1001` |\n\n> JPYC (Polygon/Kaia) settles 1:1 to JPY \u2014 no FX conversion.\n\n> Test keys (`bp_test_\u2026`) accept only testnet values; live keys only mainnet."
|
|
4836
4871
|
},
|
|
4837
4872
|
{
|
|
4838
4873
|
page: "reference",
|
|
4839
4874
|
pageTitle: "Chains & contracts",
|
|
4840
4875
|
heading: "Contract addresses",
|
|
4841
4876
|
url: "https://bitpal.io/docs/#reference",
|
|
4842
|
-
text: "## Contract addresses\n\nThe deposit-address contracts (EVM/Tron deterministic forwarder factory, Solana program). Published here as each network goes live.\n\n| Network | Contract / program | Mainnet | Testnet |\n| --- | --- | --- | --- |\n| Base | Forwarder factory | `0xC5d358f4346d59D8eCDb5cE7DE77367b068a7E7c` | `0x5bA4F84B3002B9F94670f296134aE4f823662B9f` |\n| Arbitrum | Forwarder factory | `0xC7989977010890AE78938a84E487573a1D993366` | `0x38B54b7F8a032E8087cd04f12a7Bdb9011c5Bd95` |\n| BNB | Forwarder factory | `0xC7989977010890AE78938a84E487573a1D993366` | `0xeD24d9ED87a9937528520413891e8f78EF7ED3C5` |\n| Tron | Forwarder factory |
|
|
4877
|
+
text: "## Contract addresses\n\nThe deposit-address contracts (EVM/Tron deterministic forwarder factory, Solana program). Published here as each network goes live.\n\n| Network | Contract / program | Mainnet | Testnet |\n| --- | --- | --- | --- |\n| Base | Forwarder factory | `0xC5d358f4346d59D8eCDb5cE7DE77367b068a7E7c` | `0x5bA4F84B3002B9F94670f296134aE4f823662B9f` |\n| Arbitrum | Forwarder factory | `0xC7989977010890AE78938a84E487573a1D993366` | `0x38B54b7F8a032E8087cd04f12a7Bdb9011c5Bd95` |\n| Optimism | Forwarder factory | `0x3Cfb82D593A6ba4D2CE2C17bD5A71A99D15797cC` | `0xC7989977010890AE78938a84E487573a1D993366` |\n| BNB | Forwarder factory | `0xC7989977010890AE78938a84E487573a1D993366` | `0xeD24d9ED87a9937528520413891e8f78EF7ED3C5` |\n| Tron | Forwarder factory | Coming soon | `TMwTqCDtWqA2jJ3Qii3hS2ADMSem4Dnubj` |\n| Solana | Program ID | `2cZyQxMgWToiCWho9q24VyAvs6xy7t3JSEEwNpGjf4ec` | `2cZyQxMgWToiCWho9q24VyAvs6xy7t3JSEEwNpGjf4ec` |\n| Polygon | Forwarder factory | `0xA2F1bB858DBc25355D8cb5180D7e58bDC2D696De` | `0x42A70E1a47C9AAeF10923f5345b2ad64D2F05D3F` |\n| Kaia | Forwarder factory | `0xA2F1bB858DBc25355D8cb5180D7e58bDC2D696De` | `0xf07BB5243636D8C834b31f224E2567E6e6aF38ce` |\n\nThese are the factories that derive deposit addresses. Always treat the deposit address returned by BitPal as authoritative for a given order \u2014 funds are cryptographically committed to your destination."
|
|
4843
4878
|
},
|
|
4844
4879
|
{
|
|
4845
4880
|
page: "reference",
|
|
4846
4881
|
pageTitle: "Chains & contracts",
|
|
4847
4882
|
heading: "Refunds & edge cases",
|
|
4848
4883
|
url: "https://bitpal.io/docs/#reference",
|
|
4849
|
-
text: '## Refunds & edge cases\n\nPayments are push transfers, so a buyer can pay the wrong amount or token. BitPal resolves each case automatically to the buyer\'s refund address (set at pay time, committed & immutable).\n\n| Case | What happens |\n| --- | --- |\n| **Overpaid** | Settles the exact amount; surplus refunded. |\n| **Underpaid** | Open for a top-up; refunded if the deadline passes. |\n| **Wrong token (another supported stablecoin)** | Returned to the buyer; order stays open. |\n| **Unsupported token** | Not detected and not returned automatically; on EVM and Tron it can be returned on request (contact support). |\n| **Native coin (ETH, BNB, TRX, SOL \u2026)** | Not detected and cannot be recovered by BitPal. |\n| **Dust (0.0001 token or less)** | Not refunded automatically \u2014 the network fee would exceed it. |\n| **Expired** | No funds arrived \u2014 address reclaimed. |\n| **Late, full amount** | Accepted as a late payment for up to 7 days (`late: true`); settles normally. Not for sessions that already had a wrong-token return or underpayment refund; after the cutoff it is refunded (`late_deposit`). |\n| **Late, full amount, `late_payments: "refund"`** | Not accepted.
|
|
4884
|
+
text: '## Refunds & edge cases\n\nPayments are push transfers, so a buyer can pay the wrong amount or token. BitPal resolves each case automatically to the buyer\'s refund address (set at pay time, committed & immutable).\n\n| Case | What happens |\n| --- | --- |\n| **Overpaid** | Settles the exact amount; surplus refunded. |\n| **Underpaid** | Open for a top-up; refunded if the deadline passes. |\n| **Wrong token (another supported stablecoin)** | Returned to the buyer; order stays open. |\n| **Unsupported token** | Not detected and not returned automatically; on EVM and Tron it can be returned on request (contact support). |\n| **Native coin (ETH, BNB, TRX, SOL \u2026)** | Not detected and cannot be recovered by BitPal. |\n| **Dust (0.0001 token or less)** | Not refunded automatically \u2014 the network fee would exceed it. |\n| **Expired** | No funds arrived \u2014 address reclaimed. |\n| **Late, full amount** | Accepted as a late payment for up to 7 days (`late: true`); settles normally. Not for sessions that already had a wrong-token return or underpayment refund; after the cutoff it is refunded (`late_deposit`). |\n| **Late, full amount, `late_payments: "refund"`** | Not accepted. `checkout.session.unresolved` (`reason: "late"`) right away, then refunded automatically about 7 days after the deadline (`late_deposit`) \u2014 the non-custodial deposit contract does not allow an earlier refund. |\n| **Late, partial** | Refunded (`late_deposit`). |\n| **Customer return (completed payment)** | No refund API \u2014 BitPal never holds the funds. Refund in Console \u2192 Sessions \u2192 session \u2192 Refund: send from your own wallet, record the transaction, and BitPal verifies it and sends `checkout.refund.recorded` (`merchant`). You pay the gas and the refund; the BitPal fee is not returned. |'
|
|
4850
4885
|
},
|
|
4851
4886
|
{
|
|
4852
4887
|
page: "reference",
|
|
@@ -4888,7 +4923,7 @@ var docs_data_default = {
|
|
|
4888
4923
|
pageTitle: "SDK",
|
|
4889
4924
|
heading: "Example: poll status",
|
|
4890
4925
|
url: "https://bitpal.io/docs/#sdk",
|
|
4891
|
-
text: "## Example: poll status\n\n```\nconst { data } = await bitpal.checkout.getStatus(sessionId);\nif (data.status === 'paid_confirmed' || data.status === 'swept') showSuccess(); // UI only \u2014 fulfil from the webhook\n```\n\nStatus values: `created` \u2192 `awaiting_deposit` \u2192 `deposit_detected` \u2192 `paid_confirmed` \u2192 `swept`, plus `expired`, `unresolved_underpaid
|
|
4926
|
+
text: "## Example: poll status\n\n```\nconst { data } = await bitpal.checkout.getStatus(sessionId);\nif (data.status === 'paid_confirmed' || data.status === 'swept') showSuccess(); // UI only \u2014 fulfil from the webhook\n```\n\nStatus values: `created` \u2192 `awaiting_deposit` \u2192 `deposit_detected` \u2192 `paid_confirmed` \u2192 `swept`, plus `expired`, `unresolved_underpaid` (and `wrong_token` on sessions before 2026-11-12). (Sessions opened through the legacy wallet-connect flow may report `pending_payment`; treat it like `created`.) See [Payment lifecycle](/docs/md/lifecycle.md)."
|
|
4892
4927
|
},
|
|
4893
4928
|
{
|
|
4894
4929
|
page: "sdk",
|
|
@@ -4965,7 +5000,7 @@ var docs_data_default = {
|
|
|
4965
5000
|
pageTitle: "Webhooks",
|
|
4966
5001
|
heading: "Event data",
|
|
4967
5002
|
url: "https://bitpal.io/docs/#webhooks",
|
|
4968
|
-
text:
|
|
5003
|
+
text: '## Event data\n\nEvery `data` object has `session_id` and `metadata` \u2014 the `metadata` you set when creating the session (`{}` if none), so you can map an event to your order without a lookup. BitPal\'s reserved keys (`_bitpal`, `payer_email`, `allowed_pay_chains`, `allowed_assets`) are left out. Session events also carry `amount_total` (6-decimal integer string), `currency` and `late`. Deposit events (`deposit_detected`, `paid`, `unresolved`, `settled`, `expired`) also carry `deposit_tx_hash`, `received_token_symbol` and `token_decimals` \u2014 the same names as on the session (`tx_hash` is still sent with the same value). Token amounts are in the token\'s own decimals (`token_decimals`; 18 on BNB Chain USDC/USDT and JPYC) \u2014 see [Payment lifecycle \u2192 Amounts](/docs/md/lifecycle.md).\n\n| Event | Extra fields in `data` |\n| --- | --- |\n| `checkout.session.created` | \u2014 (only `session_id`, `amount_total`, `currency`) |\n| `checkout.session.deposit_detected` | `tx_hash`, `amount_received`, `received_token` (token contract) \u2014 optional event, see above |\n| `checkout.session.paid` | `deposit_tx_hash` (= `tx_hash`), `received_token_symbol`, `token_decimals`, `late` |\n| `checkout.session.settled` | `sweep_tx_hash` |\n| `checkout.session.unresolved` | `reason`: `underpaid` (+ `amount_received`, `amount_expected`, `received_token`, `deposit_tx_hash`) \xB7 `late` (a full payment arrived after the deadline on a `late_payments: "refund"` session \u2014 not accepted, refunded automatically at `refund_available_at`; sent once; + `amount_received`, `received_token`, `deposit_tx_hash`, `refund_available_at`) \xB7 `reorg` |\n| `checkout.session.expired` | `reason` (`no_deposit` \xB7 `underpaid` \xB7 `late_payment_refunded`), `expired_at`, `source` on every expiry; `refund_tx_hash` when what was received was refunded |\n| `checkout.refund.recorded` | `kind` (`overpay_excess` \xB7 `underpay_expired` \xB7 `wrong_token` \xB7 `late_deposit` \xB7 `merchant`), `amount` (in `token_decimals`), `token`, `token_symbol`, `token_decimals`, `tx_hash`, `refund_address`; for `merchant` also `from_address` and `reason_code` |\n| Legacy: sessions created before 2026-11-12 may also send `checkout.session.unresolved` with `reason: "wrong_token"`. A wrong token now arrives as `checkout.refund.recorded` with `kind: "wrong_token"` and the session stays `awaiting_deposit`. |'
|
|
4969
5004
|
},
|
|
4970
5005
|
{
|
|
4971
5006
|
page: "webhooks",
|
|
@@ -5304,6 +5339,10 @@ var docs_data_default = {
|
|
|
5304
5339
|
name: "Payment links",
|
|
5305
5340
|
description: "Shareable hosted URLs for a price."
|
|
5306
5341
|
},
|
|
5342
|
+
{
|
|
5343
|
+
name: "Customers",
|
|
5344
|
+
description: "Buyers who paid you (created automatically from paid sessions) and their recent payments. Scoped to the current mode."
|
|
5345
|
+
},
|
|
5307
5346
|
{
|
|
5308
5347
|
name: "x402",
|
|
5309
5348
|
description: "Charge per API call with HTTP 402 and EIP-3009 authorizations."
|
|
@@ -6773,6 +6812,327 @@ var docs_data_default = {
|
|
|
6773
6812
|
}
|
|
6774
6813
|
}
|
|
6775
6814
|
},
|
|
6815
|
+
"/v1/checkout/customers": {
|
|
6816
|
+
post: {
|
|
6817
|
+
operationId: "createCustomer",
|
|
6818
|
+
tags: [
|
|
6819
|
+
"Customers"
|
|
6820
|
+
],
|
|
6821
|
+
summary: "Create a customer",
|
|
6822
|
+
description: "Customers are also created automatically from paid sessions (by payer wallet, or by email when there is no wallet). A wallet address can exist once per mode \u2014 a duplicate returns 409 `CUSTOMER_ALREADY_EXISTS`.",
|
|
6823
|
+
security: [
|
|
6824
|
+
{
|
|
6825
|
+
apiKey: []
|
|
6826
|
+
}
|
|
6827
|
+
],
|
|
6828
|
+
"x-bitpal-auth": "apiKey",
|
|
6829
|
+
parameters: [
|
|
6830
|
+
{
|
|
6831
|
+
name: "Idempotency-Key",
|
|
6832
|
+
in: "header",
|
|
6833
|
+
required: false,
|
|
6834
|
+
description: "Accepted but not yet enforced on this endpoint \u2014 retries are not de-duplicated by key.",
|
|
6835
|
+
schema: {
|
|
6836
|
+
type: "string",
|
|
6837
|
+
minLength: 8,
|
|
6838
|
+
maxLength: 128
|
|
6839
|
+
}
|
|
6840
|
+
}
|
|
6841
|
+
],
|
|
6842
|
+
requestBody: {
|
|
6843
|
+
required: true,
|
|
6844
|
+
content: {
|
|
6845
|
+
"application/json": {
|
|
6846
|
+
schema: {
|
|
6847
|
+
$ref: "#/components/schemas/CreateCustomerRequest"
|
|
6848
|
+
},
|
|
6849
|
+
example: {
|
|
6850
|
+
wallet_address: "0x1111111111111111111111111111111111111111",
|
|
6851
|
+
email: "buyer@example.com"
|
|
6852
|
+
}
|
|
6853
|
+
}
|
|
6854
|
+
}
|
|
6855
|
+
},
|
|
6856
|
+
responses: {
|
|
6857
|
+
"201": {
|
|
6858
|
+
description: "Customer created.",
|
|
6859
|
+
content: {
|
|
6860
|
+
"application/json": {
|
|
6861
|
+
schema: {
|
|
6862
|
+
type: "object",
|
|
6863
|
+
properties: {
|
|
6864
|
+
data: {
|
|
6865
|
+
$ref: "#/components/schemas/Customer"
|
|
6866
|
+
},
|
|
6867
|
+
meta: {
|
|
6868
|
+
$ref: "#/components/schemas/ResponseMeta",
|
|
6869
|
+
description: "Added to responses of authenticated endpoints. List endpoints may also carry total/limit/offset."
|
|
6870
|
+
}
|
|
6871
|
+
},
|
|
6872
|
+
required: [
|
|
6873
|
+
"data"
|
|
6874
|
+
]
|
|
6875
|
+
}
|
|
6876
|
+
}
|
|
6877
|
+
}
|
|
6878
|
+
},
|
|
6879
|
+
"400": {
|
|
6880
|
+
$ref: "#/components/responses/Error400"
|
|
6881
|
+
},
|
|
6882
|
+
"401": {
|
|
6883
|
+
$ref: "#/components/responses/Error401"
|
|
6884
|
+
},
|
|
6885
|
+
"409": {
|
|
6886
|
+
$ref: "#/components/responses/Error409"
|
|
6887
|
+
}
|
|
6888
|
+
}
|
|
6889
|
+
},
|
|
6890
|
+
get: {
|
|
6891
|
+
operationId: "listCustomers",
|
|
6892
|
+
tags: [
|
|
6893
|
+
"Customers"
|
|
6894
|
+
],
|
|
6895
|
+
summary: "List customers",
|
|
6896
|
+
security: [
|
|
6897
|
+
{
|
|
6898
|
+
apiKey: []
|
|
6899
|
+
}
|
|
6900
|
+
],
|
|
6901
|
+
"x-bitpal-auth": "apiKey",
|
|
6902
|
+
parameters: [
|
|
6903
|
+
{
|
|
6904
|
+
name: "limit",
|
|
6905
|
+
in: "query",
|
|
6906
|
+
required: false,
|
|
6907
|
+
schema: {
|
|
6908
|
+
type: "integer",
|
|
6909
|
+
minimum: 1,
|
|
6910
|
+
maximum: 100,
|
|
6911
|
+
default: 20,
|
|
6912
|
+
description: "1\u2013100, default 20."
|
|
6913
|
+
}
|
|
6914
|
+
},
|
|
6915
|
+
{
|
|
6916
|
+
name: "offset",
|
|
6917
|
+
in: "query",
|
|
6918
|
+
required: false,
|
|
6919
|
+
schema: {
|
|
6920
|
+
type: "integer",
|
|
6921
|
+
minimum: 0,
|
|
6922
|
+
description: "Rows to skip, default 0."
|
|
6923
|
+
}
|
|
6924
|
+
},
|
|
6925
|
+
{
|
|
6926
|
+
name: "page",
|
|
6927
|
+
in: "query",
|
|
6928
|
+
required: false,
|
|
6929
|
+
schema: {
|
|
6930
|
+
type: "integer",
|
|
6931
|
+
minimum: 1,
|
|
6932
|
+
description: "Deprecated \u2014 use `offset`. 1-based page, used only when `offset` is absent."
|
|
6933
|
+
}
|
|
6934
|
+
},
|
|
6935
|
+
{
|
|
6936
|
+
name: "search",
|
|
6937
|
+
in: "query",
|
|
6938
|
+
required: false,
|
|
6939
|
+
schema: {
|
|
6940
|
+
type: "string",
|
|
6941
|
+
minLength: 1,
|
|
6942
|
+
maxLength: 200,
|
|
6943
|
+
description: "Matches email or wallet address (case-insensitive, partial)."
|
|
6944
|
+
}
|
|
6945
|
+
},
|
|
6946
|
+
{
|
|
6947
|
+
name: "sort",
|
|
6948
|
+
in: "query",
|
|
6949
|
+
required: false,
|
|
6950
|
+
schema: {
|
|
6951
|
+
type: "string",
|
|
6952
|
+
enum: [
|
|
6953
|
+
"created_at",
|
|
6954
|
+
"email",
|
|
6955
|
+
"name",
|
|
6956
|
+
"wallet_address",
|
|
6957
|
+
"total_spent",
|
|
6958
|
+
"transaction_count"
|
|
6959
|
+
],
|
|
6960
|
+
default: "created_at"
|
|
6961
|
+
}
|
|
6962
|
+
},
|
|
6963
|
+
{
|
|
6964
|
+
name: "order",
|
|
6965
|
+
in: "query",
|
|
6966
|
+
required: false,
|
|
6967
|
+
schema: {
|
|
6968
|
+
type: "string",
|
|
6969
|
+
enum: [
|
|
6970
|
+
"asc",
|
|
6971
|
+
"desc"
|
|
6972
|
+
],
|
|
6973
|
+
default: "desc"
|
|
6974
|
+
}
|
|
6975
|
+
}
|
|
6976
|
+
],
|
|
6977
|
+
responses: {
|
|
6978
|
+
"200": {
|
|
6979
|
+
description: "Customers in the current mode, with `meta.total` for paging.",
|
|
6980
|
+
content: {
|
|
6981
|
+
"application/json": {
|
|
6982
|
+
schema: {
|
|
6983
|
+
type: "object",
|
|
6984
|
+
properties: {
|
|
6985
|
+
data: {
|
|
6986
|
+
type: "array",
|
|
6987
|
+
items: {
|
|
6988
|
+
$ref: "#/components/schemas/Customer"
|
|
6989
|
+
}
|
|
6990
|
+
},
|
|
6991
|
+
meta: {
|
|
6992
|
+
$ref: "#/components/schemas/ResponseMeta",
|
|
6993
|
+
description: "Added to responses of authenticated endpoints. List endpoints may also carry total/limit/offset."
|
|
6994
|
+
}
|
|
6995
|
+
},
|
|
6996
|
+
required: [
|
|
6997
|
+
"data"
|
|
6998
|
+
]
|
|
6999
|
+
}
|
|
7000
|
+
}
|
|
7001
|
+
}
|
|
7002
|
+
},
|
|
7003
|
+
"400": {
|
|
7004
|
+
$ref: "#/components/responses/Error400"
|
|
7005
|
+
},
|
|
7006
|
+
"401": {
|
|
7007
|
+
$ref: "#/components/responses/Error401"
|
|
7008
|
+
}
|
|
7009
|
+
}
|
|
7010
|
+
}
|
|
7011
|
+
},
|
|
7012
|
+
"/v1/checkout/customers/{id}": {
|
|
7013
|
+
get: {
|
|
7014
|
+
operationId: "getCustomer",
|
|
7015
|
+
tags: [
|
|
7016
|
+
"Customers"
|
|
7017
|
+
],
|
|
7018
|
+
summary: "Retrieve a customer",
|
|
7019
|
+
security: [
|
|
7020
|
+
{
|
|
7021
|
+
apiKey: []
|
|
7022
|
+
}
|
|
7023
|
+
],
|
|
7024
|
+
"x-bitpal-auth": "apiKey",
|
|
7025
|
+
parameters: [
|
|
7026
|
+
{
|
|
7027
|
+
name: "id",
|
|
7028
|
+
in: "path",
|
|
7029
|
+
required: true,
|
|
7030
|
+
description: "Customer id (uuid).",
|
|
7031
|
+
schema: {
|
|
7032
|
+
type: "string"
|
|
7033
|
+
}
|
|
7034
|
+
}
|
|
7035
|
+
],
|
|
7036
|
+
responses: {
|
|
7037
|
+
"200": {
|
|
7038
|
+
description: "The customer and their recent payments.",
|
|
7039
|
+
content: {
|
|
7040
|
+
"application/json": {
|
|
7041
|
+
schema: {
|
|
7042
|
+
type: "object",
|
|
7043
|
+
properties: {
|
|
7044
|
+
data: {
|
|
7045
|
+
$ref: "#/components/schemas/CustomerDetail"
|
|
7046
|
+
},
|
|
7047
|
+
meta: {
|
|
7048
|
+
$ref: "#/components/schemas/ResponseMeta",
|
|
7049
|
+
description: "Added to responses of authenticated endpoints. List endpoints may also carry total/limit/offset."
|
|
7050
|
+
}
|
|
7051
|
+
},
|
|
7052
|
+
required: [
|
|
7053
|
+
"data"
|
|
7054
|
+
]
|
|
7055
|
+
}
|
|
7056
|
+
}
|
|
7057
|
+
}
|
|
7058
|
+
},
|
|
7059
|
+
"401": {
|
|
7060
|
+
$ref: "#/components/responses/Error401"
|
|
7061
|
+
},
|
|
7062
|
+
"404": {
|
|
7063
|
+
$ref: "#/components/responses/Error404"
|
|
7064
|
+
}
|
|
7065
|
+
}
|
|
7066
|
+
},
|
|
7067
|
+
patch: {
|
|
7068
|
+
operationId: "updateCustomer",
|
|
7069
|
+
tags: [
|
|
7070
|
+
"Customers"
|
|
7071
|
+
],
|
|
7072
|
+
summary: "Update a customer",
|
|
7073
|
+
description: "Only the fields you send change. Send `null` to clear a field. The wallet address cannot change.",
|
|
7074
|
+
security: [
|
|
7075
|
+
{
|
|
7076
|
+
apiKey: []
|
|
7077
|
+
}
|
|
7078
|
+
],
|
|
7079
|
+
"x-bitpal-auth": "apiKey",
|
|
7080
|
+
parameters: [
|
|
7081
|
+
{
|
|
7082
|
+
name: "id",
|
|
7083
|
+
in: "path",
|
|
7084
|
+
required: true,
|
|
7085
|
+
description: "Customer id (uuid).",
|
|
7086
|
+
schema: {
|
|
7087
|
+
type: "string"
|
|
7088
|
+
}
|
|
7089
|
+
}
|
|
7090
|
+
],
|
|
7091
|
+
requestBody: {
|
|
7092
|
+
required: true,
|
|
7093
|
+
content: {
|
|
7094
|
+
"application/json": {
|
|
7095
|
+
schema: {
|
|
7096
|
+
$ref: "#/components/schemas/UpdateCustomerRequest"
|
|
7097
|
+
}
|
|
7098
|
+
}
|
|
7099
|
+
}
|
|
7100
|
+
},
|
|
7101
|
+
responses: {
|
|
7102
|
+
"200": {
|
|
7103
|
+
description: "The updated customer.",
|
|
7104
|
+
content: {
|
|
7105
|
+
"application/json": {
|
|
7106
|
+
schema: {
|
|
7107
|
+
type: "object",
|
|
7108
|
+
properties: {
|
|
7109
|
+
data: {
|
|
7110
|
+
$ref: "#/components/schemas/Customer"
|
|
7111
|
+
},
|
|
7112
|
+
meta: {
|
|
7113
|
+
$ref: "#/components/schemas/ResponseMeta",
|
|
7114
|
+
description: "Added to responses of authenticated endpoints. List endpoints may also carry total/limit/offset."
|
|
7115
|
+
}
|
|
7116
|
+
},
|
|
7117
|
+
required: [
|
|
7118
|
+
"data"
|
|
7119
|
+
]
|
|
7120
|
+
}
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
7123
|
+
},
|
|
7124
|
+
"400": {
|
|
7125
|
+
$ref: "#/components/responses/Error400"
|
|
7126
|
+
},
|
|
7127
|
+
"401": {
|
|
7128
|
+
$ref: "#/components/responses/Error401"
|
|
7129
|
+
},
|
|
7130
|
+
"404": {
|
|
7131
|
+
$ref: "#/components/responses/Error404"
|
|
7132
|
+
}
|
|
7133
|
+
}
|
|
7134
|
+
}
|
|
7135
|
+
},
|
|
6776
7136
|
"/v1/x402/assets": {
|
|
6777
7137
|
get: {
|
|
6778
7138
|
operationId: "listX402Assets",
|
|
@@ -7913,7 +8273,7 @@ var docs_data_default = {
|
|
|
7913
8273
|
"Sandbox"
|
|
7914
8274
|
],
|
|
7915
8275
|
summary: "Simulate a buyer payment",
|
|
7916
|
-
description:
|
|
8276
|
+
description: 'Test mode only: BitPal sends a testnet deposit to the session for you, so you can test fulfilment and webhooks without a wallet. Scenarios: `exact` (pays the rest \u2014 the session gets paid), `underpay` (half of the rest), `overpay` (the rest + 1 token \u2014 the excess is refunded), `wrong_token` (1 unit of another token on the same chain \u2014 returned; the session stays payable). Issues the deposit address first if needed (a scenario the chain cannot run is rejected before an address is issued). `wrong_token` needs a second test token on the chain \u2014 today BNB Smart Chain Testnet and Polygon Amoy; elsewhere 400 `SANDBOX_SCENARIO_UNAVAILABLE` lists the chains that work (`details.supported_chains`). **Late payments:** on a session whose deposit address was issued and that then expired unpaid (`expires_at` + 90 min), `exact`/`overpay`/`underpay` send a payment after the deadline \u2014 `late_payments: "accept"` \u2192 `checkout.session.paid` with `late: true`; `"refund"` \u2192 `checkout.session.unresolved` with `reason: "late"` and an automatic refund about 7 days after the deadline. A session that expired without an address cannot be paid (409 `SANDBOX_SESSION_NOT_PAYABLE`). A `bp_live_` key gets 403 `SANDBOX_TEST_MODE_ONLY`. Limited to 10 calls per minute and 300 per day per account.',
|
|
7917
8277
|
security: [
|
|
7918
8278
|
{
|
|
7919
8279
|
apiKey: []
|
|
@@ -8852,6 +9212,9 @@ var docs_data_default = {
|
|
|
8852
9212
|
amount_total: "1000000",
|
|
8853
9213
|
currency: "USDC",
|
|
8854
9214
|
late: false,
|
|
9215
|
+
deposit_tx_hash: "0xd9a75dfe753b20027a0a93841c4e70af1b6be061b59e530d1bc381e88c196fe3",
|
|
9216
|
+
received_token_symbol: "USDC",
|
|
9217
|
+
token_decimals: 6,
|
|
8855
9218
|
tx_hash: "0xd9a75dfe753b20027a0a93841c4e70af1b6be061b59e530d1bc381e88c196fe3",
|
|
8856
9219
|
metadata: {
|
|
8857
9220
|
order_id: "ord_1042"
|
|
@@ -8970,7 +9333,7 @@ var docs_data_default = {
|
|
|
8970
9333
|
"Webhook events"
|
|
8971
9334
|
],
|
|
8972
9335
|
summary: "checkout.session.unresolved",
|
|
8973
|
-
description: "
|
|
9336
|
+
description: 'Needs attention (`reason`): `underpaid`, `late` (a full payment arrived after the deadline on a session created with `late_payments: "refund"` \u2014 it is not accepted and is refunded automatically at `refund_available_at`; sent once), or `reorg` (a confirmed deposit was reverted). `wrong_token` only on sessions created before 2026-11-12.',
|
|
8974
9337
|
parameters: [
|
|
8975
9338
|
{
|
|
8976
9339
|
name: "X-Webhook-Signature-256",
|
|
@@ -9061,6 +9424,9 @@ var docs_data_default = {
|
|
|
9061
9424
|
amount_expected: "5000000",
|
|
9062
9425
|
amount_received: "2500000",
|
|
9063
9426
|
received_token: "0x943b94Dacf1507F2658f1CfE103Ac36Ebc13Ef65",
|
|
9427
|
+
received_token_symbol: "USDC",
|
|
9428
|
+
token_decimals: 6,
|
|
9429
|
+
deposit_tx_hash: "0x4461291b5d743d3950a474c015098b8cb53b4373346bd510f74481dd8dbb9d65",
|
|
9064
9430
|
tx_hash: "0x4461291b5d743d3950a474c015098b8cb53b4373346bd510f74481dd8dbb9d65",
|
|
9065
9431
|
metadata: {
|
|
9066
9432
|
order_id: "ord_1041"
|
|
@@ -9266,6 +9632,8 @@ var docs_data_default = {
|
|
|
9266
9632
|
kind: "overpay_excess",
|
|
9267
9633
|
amount: "1000000",
|
|
9268
9634
|
token: "0x943b94Dacf1507F2658f1CfE103Ac36Ebc13Ef65",
|
|
9635
|
+
token_symbol: "USDC",
|
|
9636
|
+
token_decimals: 6,
|
|
9269
9637
|
tx_hash: "0xe943412e2be8b3e3fc6359b47a8eea0cc4de17768b069f52ebab1aacd873aec4",
|
|
9270
9638
|
refund_address: "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720",
|
|
9271
9639
|
metadata: {
|
|
@@ -9672,11 +10040,25 @@ var docs_data_default = {
|
|
|
9672
10040
|
},
|
|
9673
10041
|
amount: {
|
|
9674
10042
|
type: "string",
|
|
9675
|
-
description: "Atomic amount in the
|
|
10043
|
+
description: "Atomic amount in the refunded token\u2019s own decimals \u2014 see `token_decimals` (BNB Chain USDC/USDT and JPYC = 18, while `amount_total` on the session is always 6-decimal)."
|
|
9676
10044
|
},
|
|
9677
10045
|
token: {
|
|
9678
10046
|
type: "string"
|
|
9679
10047
|
},
|
|
10048
|
+
token_symbol: {
|
|
10049
|
+
type: [
|
|
10050
|
+
"string",
|
|
10051
|
+
"null"
|
|
10052
|
+
],
|
|
10053
|
+
description: "Symbol of `token` (e.g. `USDC`). Null if BitPal does not know the token."
|
|
10054
|
+
},
|
|
10055
|
+
token_decimals: {
|
|
10056
|
+
type: [
|
|
10057
|
+
"integer",
|
|
10058
|
+
"null"
|
|
10059
|
+
],
|
|
10060
|
+
description: "Decimals of `amount`. Null if BitPal does not know the token."
|
|
10061
|
+
},
|
|
9680
10062
|
tx_hash: {
|
|
9681
10063
|
type: "string"
|
|
9682
10064
|
},
|
|
@@ -9704,6 +10086,8 @@ var docs_data_default = {
|
|
|
9704
10086
|
"kind",
|
|
9705
10087
|
"amount",
|
|
9706
10088
|
"token",
|
|
10089
|
+
"token_symbol",
|
|
10090
|
+
"token_decimals",
|
|
9707
10091
|
"tx_hash",
|
|
9708
10092
|
"refund_address"
|
|
9709
10093
|
],
|
|
@@ -9757,7 +10141,7 @@ var docs_data_default = {
|
|
|
9757
10141
|
"integer",
|
|
9758
10142
|
"null"
|
|
9759
10143
|
],
|
|
9760
|
-
description: "Decimals of the payment token (USDC/USDT = 6
|
|
10144
|
+
description: "Decimals of the payment token (most USDC/USDT = 6; BNB Chain USDT/USDC and JPYC = 18). `amount_received` is in these units; `amount_total` is always 6-decimal."
|
|
9761
10145
|
},
|
|
9762
10146
|
payment_method: {
|
|
9763
10147
|
type: [
|
|
@@ -9871,6 +10255,15 @@ var docs_data_default = {
|
|
|
9871
10255
|
],
|
|
9872
10256
|
description: "Transaction that settled the funds to your payout wallet (or refunded/rescued them)."
|
|
9873
10257
|
},
|
|
10258
|
+
refund_status: {
|
|
10259
|
+
type: "string",
|
|
10260
|
+
enum: [
|
|
10261
|
+
"none",
|
|
10262
|
+
"partially_refunded",
|
|
10263
|
+
"refunded"
|
|
10264
|
+
],
|
|
10265
|
+
description: "Refunds you sent from your own wallet and recorded in the console (`kind: merchant`). `status` stays `settled` after a refund \u2014 check this field. Automatic refunds (overpaid excess, expired underpayment, wrong token) do not count."
|
|
10266
|
+
},
|
|
9874
10267
|
success_url: {
|
|
9875
10268
|
type: [
|
|
9876
10269
|
"string",
|
|
@@ -10016,6 +10409,7 @@ var docs_data_default = {
|
|
|
10016
10409
|
"tx_hash",
|
|
10017
10410
|
"deposit_tx_hash",
|
|
10018
10411
|
"sweep_tx_hash",
|
|
10412
|
+
"refund_status",
|
|
10019
10413
|
"success_url",
|
|
10020
10414
|
"cancel_url",
|
|
10021
10415
|
"metadata",
|
|
@@ -10084,8 +10478,30 @@ var docs_data_default = {
|
|
|
10084
10478
|
type: "boolean",
|
|
10085
10479
|
description: "true when the full amount arrived after the payment window."
|
|
10086
10480
|
},
|
|
10481
|
+
deposit_tx_hash: {
|
|
10482
|
+
type: [
|
|
10483
|
+
"string",
|
|
10484
|
+
"null"
|
|
10485
|
+
],
|
|
10486
|
+
description: "The buyer\u2019s payment into the deposit address \u2014 same field as on the session. Null when nothing was received."
|
|
10487
|
+
},
|
|
10488
|
+
received_token_symbol: {
|
|
10489
|
+
type: [
|
|
10490
|
+
"string",
|
|
10491
|
+
"null"
|
|
10492
|
+
],
|
|
10493
|
+
description: "Symbol of the token received (e.g. `USDC`) \u2014 same field as on the session."
|
|
10494
|
+
},
|
|
10495
|
+
token_decimals: {
|
|
10496
|
+
type: [
|
|
10497
|
+
"integer",
|
|
10498
|
+
"null"
|
|
10499
|
+
],
|
|
10500
|
+
description: "Decimals of `amount_received` / `amount_expected` (most USDC/USDT = 6; BNB Chain USDC/USDT and JPYC = 18). `amount_total` is always 6-decimal."
|
|
10501
|
+
},
|
|
10087
10502
|
tx_hash: {
|
|
10088
|
-
type: "string"
|
|
10503
|
+
type: "string",
|
|
10504
|
+
description: "Same as `deposit_tx_hash` (kept for compatibility)."
|
|
10089
10505
|
},
|
|
10090
10506
|
amount_received: {
|
|
10091
10507
|
type: "string",
|
|
@@ -10126,38 +10542,67 @@ var docs_data_default = {
|
|
|
10126
10542
|
currency: {
|
|
10127
10543
|
type: "string"
|
|
10128
10544
|
},
|
|
10129
|
-
late: {
|
|
10130
|
-
type: "boolean",
|
|
10131
|
-
description: "true when the full amount arrived after the payment window."
|
|
10545
|
+
late: {
|
|
10546
|
+
type: "boolean",
|
|
10547
|
+
description: "true when the full amount arrived after the payment window."
|
|
10548
|
+
},
|
|
10549
|
+
deposit_tx_hash: {
|
|
10550
|
+
type: [
|
|
10551
|
+
"string",
|
|
10552
|
+
"null"
|
|
10553
|
+
],
|
|
10554
|
+
description: "The buyer\u2019s payment into the deposit address \u2014 same field as on the session. Null when nothing was received."
|
|
10555
|
+
},
|
|
10556
|
+
received_token_symbol: {
|
|
10557
|
+
type: [
|
|
10558
|
+
"string",
|
|
10559
|
+
"null"
|
|
10560
|
+
],
|
|
10561
|
+
description: "Symbol of the token received (e.g. `USDC`) \u2014 same field as on the session."
|
|
10562
|
+
},
|
|
10563
|
+
token_decimals: {
|
|
10564
|
+
type: [
|
|
10565
|
+
"integer",
|
|
10566
|
+
"null"
|
|
10567
|
+
],
|
|
10568
|
+
description: "Decimals of `amount_received` / `amount_expected` (most USDC/USDT = 6; BNB Chain USDC/USDT and JPYC = 18). `amount_total` is always 6-decimal."
|
|
10132
10569
|
},
|
|
10133
10570
|
reason: {
|
|
10134
10571
|
type: "string",
|
|
10135
|
-
|
|
10136
|
-
|
|
10572
|
+
enum: [
|
|
10573
|
+
"no_deposit",
|
|
10574
|
+
"underpaid",
|
|
10575
|
+
"late_payment_refunded"
|
|
10576
|
+
],
|
|
10577
|
+
description: "`no_deposit` = nothing was paid. `underpaid` = still short at the deadline; what was received is refunded (`refund_tx_hash`). `late_payment_refunded` = a full payment arrived after the late-acceptance cutoff and was refunded (`refund_tx_hash`)."
|
|
10137
10578
|
},
|
|
10138
10579
|
expired_at: {
|
|
10139
10580
|
type: "string",
|
|
10140
10581
|
format: "date-time",
|
|
10141
|
-
description: "When the session was marked expired.
|
|
10582
|
+
description: "When the session was marked expired."
|
|
10142
10583
|
},
|
|
10143
10584
|
source: {
|
|
10144
10585
|
type: "string",
|
|
10145
10586
|
enum: [
|
|
10146
10587
|
"lazy",
|
|
10147
|
-
"cron"
|
|
10588
|
+
"cron",
|
|
10589
|
+
"deposit_scan"
|
|
10148
10590
|
],
|
|
10149
|
-
description: "What noticed the expiry: a read of the session (`lazy`)
|
|
10591
|
+
description: "What noticed the expiry: a read of the session (`lazy`), the scheduled sweep (`cron`) or the deposit monitor (`deposit_scan`, sessions with a deposit address). Informational."
|
|
10150
10592
|
},
|
|
10151
10593
|
refund_tx_hash: {
|
|
10152
10594
|
type: "string",
|
|
10153
|
-
description: "Set when
|
|
10595
|
+
description: "Set when what was received was refunded (`underpaid`, `late_payment_refunded`)."
|
|
10154
10596
|
}
|
|
10155
10597
|
},
|
|
10156
10598
|
required: [
|
|
10157
10599
|
"session_id",
|
|
10158
10600
|
"metadata",
|
|
10159
10601
|
"amount_total",
|
|
10160
|
-
"currency"
|
|
10602
|
+
"currency",
|
|
10603
|
+
"reason",
|
|
10604
|
+
"expired_at",
|
|
10605
|
+
"source"
|
|
10161
10606
|
],
|
|
10162
10607
|
additionalProperties: true
|
|
10163
10608
|
},
|
|
@@ -10209,7 +10654,7 @@ var docs_data_default = {
|
|
|
10209
10654
|
"integer",
|
|
10210
10655
|
"null"
|
|
10211
10656
|
],
|
|
10212
|
-
description: "Decimals of the payment token (USDC/USDT = 6
|
|
10657
|
+
description: "Decimals of the payment token (most USDC/USDT = 6; BNB Chain USDT/USDC and JPYC = 18). `amount_received` is in these units; `amount_total` is always 6-decimal."
|
|
10213
10658
|
},
|
|
10214
10659
|
payment_method: {
|
|
10215
10660
|
type: [
|
|
@@ -10323,6 +10768,15 @@ var docs_data_default = {
|
|
|
10323
10768
|
],
|
|
10324
10769
|
description: "Transaction that settled the funds to your payout wallet (or refunded/rescued them)."
|
|
10325
10770
|
},
|
|
10771
|
+
refund_status: {
|
|
10772
|
+
type: "string",
|
|
10773
|
+
enum: [
|
|
10774
|
+
"none",
|
|
10775
|
+
"partially_refunded",
|
|
10776
|
+
"refunded"
|
|
10777
|
+
],
|
|
10778
|
+
description: "Refunds you sent from your own wallet and recorded in the console (`kind: merchant`). `status` stays `settled` after a refund \u2014 check this field. Automatic refunds (overpaid excess, expired underpayment, wrong token) do not count."
|
|
10779
|
+
},
|
|
10326
10780
|
success_url: {
|
|
10327
10781
|
type: [
|
|
10328
10782
|
"string",
|
|
@@ -10416,6 +10870,7 @@ var docs_data_default = {
|
|
|
10416
10870
|
"tx_hash",
|
|
10417
10871
|
"deposit_tx_hash",
|
|
10418
10872
|
"sweep_tx_hash",
|
|
10873
|
+
"refund_status",
|
|
10419
10874
|
"success_url",
|
|
10420
10875
|
"cancel_url",
|
|
10421
10876
|
"metadata",
|
|
@@ -10452,8 +10907,30 @@ var docs_data_default = {
|
|
|
10452
10907
|
type: "boolean",
|
|
10453
10908
|
description: "true when the full amount arrived after the payment window."
|
|
10454
10909
|
},
|
|
10910
|
+
deposit_tx_hash: {
|
|
10911
|
+
type: [
|
|
10912
|
+
"string",
|
|
10913
|
+
"null"
|
|
10914
|
+
],
|
|
10915
|
+
description: "The buyer\u2019s payment into the deposit address \u2014 same field as on the session. Null when nothing was received."
|
|
10916
|
+
},
|
|
10917
|
+
received_token_symbol: {
|
|
10918
|
+
type: [
|
|
10919
|
+
"string",
|
|
10920
|
+
"null"
|
|
10921
|
+
],
|
|
10922
|
+
description: "Symbol of the token received (e.g. `USDC`) \u2014 same field as on the session."
|
|
10923
|
+
},
|
|
10924
|
+
token_decimals: {
|
|
10925
|
+
type: [
|
|
10926
|
+
"integer",
|
|
10927
|
+
"null"
|
|
10928
|
+
],
|
|
10929
|
+
description: "Decimals of `amount_received` / `amount_expected` (most USDC/USDT = 6; BNB Chain USDC/USDT and JPYC = 18). `amount_total` is always 6-decimal."
|
|
10930
|
+
},
|
|
10455
10931
|
tx_hash: {
|
|
10456
|
-
type: "string"
|
|
10932
|
+
type: "string",
|
|
10933
|
+
description: "Same as `deposit_tx_hash` (kept for compatibility)."
|
|
10457
10934
|
}
|
|
10458
10935
|
},
|
|
10459
10936
|
required: [
|
|
@@ -10488,6 +10965,27 @@ var docs_data_default = {
|
|
|
10488
10965
|
type: "boolean",
|
|
10489
10966
|
description: "true when the full amount arrived after the payment window."
|
|
10490
10967
|
},
|
|
10968
|
+
deposit_tx_hash: {
|
|
10969
|
+
type: [
|
|
10970
|
+
"string",
|
|
10971
|
+
"null"
|
|
10972
|
+
],
|
|
10973
|
+
description: "The buyer\u2019s payment into the deposit address \u2014 same field as on the session. Null when nothing was received."
|
|
10974
|
+
},
|
|
10975
|
+
received_token_symbol: {
|
|
10976
|
+
type: [
|
|
10977
|
+
"string",
|
|
10978
|
+
"null"
|
|
10979
|
+
],
|
|
10980
|
+
description: "Symbol of the token received (e.g. `USDC`) \u2014 same field as on the session."
|
|
10981
|
+
},
|
|
10982
|
+
token_decimals: {
|
|
10983
|
+
type: [
|
|
10984
|
+
"integer",
|
|
10985
|
+
"null"
|
|
10986
|
+
],
|
|
10987
|
+
description: "Decimals of `amount_received` / `amount_expected` (most USDC/USDT = 6; BNB Chain USDC/USDT and JPYC = 18). `amount_total` is always 6-decimal."
|
|
10988
|
+
},
|
|
10491
10989
|
sweep_tx_hash: {
|
|
10492
10990
|
type: "string"
|
|
10493
10991
|
}
|
|
@@ -10545,12 +11043,34 @@ var docs_data_default = {
|
|
|
10545
11043
|
type: "boolean",
|
|
10546
11044
|
description: "true when the full amount arrived after the payment window."
|
|
10547
11045
|
},
|
|
11046
|
+
deposit_tx_hash: {
|
|
11047
|
+
type: [
|
|
11048
|
+
"string",
|
|
11049
|
+
"null"
|
|
11050
|
+
],
|
|
11051
|
+
description: "The buyer\u2019s payment into the deposit address \u2014 same field as on the session. Null when nothing was received."
|
|
11052
|
+
},
|
|
11053
|
+
received_token_symbol: {
|
|
11054
|
+
type: [
|
|
11055
|
+
"string",
|
|
11056
|
+
"null"
|
|
11057
|
+
],
|
|
11058
|
+
description: "Symbol of the token received (e.g. `USDC`) \u2014 same field as on the session."
|
|
11059
|
+
},
|
|
11060
|
+
token_decimals: {
|
|
11061
|
+
type: [
|
|
11062
|
+
"integer",
|
|
11063
|
+
"null"
|
|
11064
|
+
],
|
|
11065
|
+
description: "Decimals of `amount_received` / `amount_expected` (most USDC/USDT = 6; BNB Chain USDC/USDT and JPYC = 18). `amount_total` is always 6-decimal."
|
|
11066
|
+
},
|
|
10548
11067
|
reason: {
|
|
10549
11068
|
type: "string",
|
|
10550
11069
|
enum: [
|
|
10551
11070
|
"underpaid",
|
|
10552
|
-
"
|
|
10553
|
-
"reorg"
|
|
11071
|
+
"late",
|
|
11072
|
+
"reorg",
|
|
11073
|
+
"wrong_token"
|
|
10554
11074
|
]
|
|
10555
11075
|
},
|
|
10556
11076
|
amount_expected: {
|
|
@@ -10565,7 +11085,13 @@ var docs_data_default = {
|
|
|
10565
11085
|
type: "string"
|
|
10566
11086
|
},
|
|
10567
11087
|
tx_hash: {
|
|
10568
|
-
type: "string"
|
|
11088
|
+
type: "string",
|
|
11089
|
+
description: "Same as `deposit_tx_hash` (kept for compatibility)."
|
|
11090
|
+
},
|
|
11091
|
+
refund_available_at: {
|
|
11092
|
+
type: "string",
|
|
11093
|
+
format: "date-time",
|
|
11094
|
+
description: 'late only: when the automatic refund to the buyer becomes possible (about 7 days after the deadline). The refund then arrives as `checkout.refund.recorded` with `kind: "late_deposit"`.'
|
|
10569
11095
|
}
|
|
10570
11096
|
},
|
|
10571
11097
|
required: [
|
|
@@ -10748,6 +11274,55 @@ var docs_data_default = {
|
|
|
10748
11274
|
],
|
|
10749
11275
|
"x-unknown-keys": "strip"
|
|
10750
11276
|
},
|
|
11277
|
+
CreateCustomerRequest: {
|
|
11278
|
+
type: "object",
|
|
11279
|
+
properties: {
|
|
11280
|
+
wallet_address: {
|
|
11281
|
+
type: "string",
|
|
11282
|
+
minLength: 1,
|
|
11283
|
+
maxLength: 128,
|
|
11284
|
+
description: "Payer wallet (0x\u2026 / T\u2026 / base58). Unique per account and mode."
|
|
11285
|
+
},
|
|
11286
|
+
email: {
|
|
11287
|
+
type: [
|
|
11288
|
+
"string",
|
|
11289
|
+
"null"
|
|
11290
|
+
],
|
|
11291
|
+
format: "email",
|
|
11292
|
+
maxLength: 320
|
|
11293
|
+
},
|
|
11294
|
+
name: {
|
|
11295
|
+
type: [
|
|
11296
|
+
"string",
|
|
11297
|
+
"null"
|
|
11298
|
+
],
|
|
11299
|
+
maxLength: 200
|
|
11300
|
+
},
|
|
11301
|
+
phone: {
|
|
11302
|
+
type: [
|
|
11303
|
+
"string",
|
|
11304
|
+
"null"
|
|
11305
|
+
],
|
|
11306
|
+
maxLength: 50
|
|
11307
|
+
},
|
|
11308
|
+
country: {
|
|
11309
|
+
type: [
|
|
11310
|
+
"string",
|
|
11311
|
+
"null"
|
|
11312
|
+
],
|
|
11313
|
+
maxLength: 100,
|
|
11314
|
+
description: 'Free text; an ISO 3166-1 alpha-2 code such as "US" is recommended.'
|
|
11315
|
+
},
|
|
11316
|
+
metadata: {
|
|
11317
|
+
type: "object",
|
|
11318
|
+
additionalProperties: true
|
|
11319
|
+
}
|
|
11320
|
+
},
|
|
11321
|
+
required: [
|
|
11322
|
+
"wallet_address"
|
|
11323
|
+
],
|
|
11324
|
+
additionalProperties: false
|
|
11325
|
+
},
|
|
10751
11326
|
CreatePaymentLinkRequest: {
|
|
10752
11327
|
type: "object",
|
|
10753
11328
|
properties: {
|
|
@@ -11038,6 +11613,136 @@ var docs_data_default = {
|
|
|
11038
11613
|
"tax"
|
|
11039
11614
|
]
|
|
11040
11615
|
},
|
|
11616
|
+
Customer: {
|
|
11617
|
+
type: "object",
|
|
11618
|
+
properties: {
|
|
11619
|
+
id: {
|
|
11620
|
+
type: "string",
|
|
11621
|
+
description: "Customer id (uuid)."
|
|
11622
|
+
},
|
|
11623
|
+
wallet_address: {
|
|
11624
|
+
type: [
|
|
11625
|
+
"string",
|
|
11626
|
+
"null"
|
|
11627
|
+
],
|
|
11628
|
+
description: "Payer wallet. Null for customers known only by email."
|
|
11629
|
+
},
|
|
11630
|
+
email: {
|
|
11631
|
+
type: [
|
|
11632
|
+
"string",
|
|
11633
|
+
"null"
|
|
11634
|
+
]
|
|
11635
|
+
},
|
|
11636
|
+
name: {
|
|
11637
|
+
type: [
|
|
11638
|
+
"string",
|
|
11639
|
+
"null"
|
|
11640
|
+
]
|
|
11641
|
+
},
|
|
11642
|
+
phone: {
|
|
11643
|
+
type: [
|
|
11644
|
+
"string",
|
|
11645
|
+
"null"
|
|
11646
|
+
]
|
|
11647
|
+
},
|
|
11648
|
+
country: {
|
|
11649
|
+
type: [
|
|
11650
|
+
"string",
|
|
11651
|
+
"null"
|
|
11652
|
+
]
|
|
11653
|
+
},
|
|
11654
|
+
metadata: {
|
|
11655
|
+
type: "object",
|
|
11656
|
+
additionalProperties: true
|
|
11657
|
+
},
|
|
11658
|
+
status: {
|
|
11659
|
+
type: "string",
|
|
11660
|
+
enum: [
|
|
11661
|
+
"active",
|
|
11662
|
+
"inactive"
|
|
11663
|
+
]
|
|
11664
|
+
},
|
|
11665
|
+
total_spent: {
|
|
11666
|
+
type: "string",
|
|
11667
|
+
pattern: "^\\d+$",
|
|
11668
|
+
description: 'Sum of paid sessions in canonical 6-decimal units ($1 = "1000000").'
|
|
11669
|
+
},
|
|
11670
|
+
transaction_count: {
|
|
11671
|
+
type: "integer"
|
|
11672
|
+
},
|
|
11673
|
+
first_payment_at: {
|
|
11674
|
+
type: [
|
|
11675
|
+
"string",
|
|
11676
|
+
"null"
|
|
11677
|
+
],
|
|
11678
|
+
format: "date-time",
|
|
11679
|
+
description: "ISO-8601 timestamp"
|
|
11680
|
+
},
|
|
11681
|
+
last_payment_at: {
|
|
11682
|
+
type: [
|
|
11683
|
+
"string",
|
|
11684
|
+
"null"
|
|
11685
|
+
],
|
|
11686
|
+
format: "date-time",
|
|
11687
|
+
description: "ISO-8601 timestamp"
|
|
11688
|
+
},
|
|
11689
|
+
mode: {
|
|
11690
|
+
type: "string",
|
|
11691
|
+
enum: [
|
|
11692
|
+
"TEST",
|
|
11693
|
+
"LIVE"
|
|
11694
|
+
]
|
|
11695
|
+
},
|
|
11696
|
+
created_at: {
|
|
11697
|
+
type: "string",
|
|
11698
|
+
format: "date-time",
|
|
11699
|
+
description: "ISO-8601 timestamp"
|
|
11700
|
+
},
|
|
11701
|
+
updated_at: {
|
|
11702
|
+
type: "string",
|
|
11703
|
+
format: "date-time",
|
|
11704
|
+
description: "ISO-8601 timestamp"
|
|
11705
|
+
}
|
|
11706
|
+
},
|
|
11707
|
+
required: [
|
|
11708
|
+
"id",
|
|
11709
|
+
"wallet_address",
|
|
11710
|
+
"email",
|
|
11711
|
+
"name",
|
|
11712
|
+
"phone",
|
|
11713
|
+
"country",
|
|
11714
|
+
"metadata",
|
|
11715
|
+
"status",
|
|
11716
|
+
"total_spent",
|
|
11717
|
+
"transaction_count",
|
|
11718
|
+
"first_payment_at",
|
|
11719
|
+
"last_payment_at",
|
|
11720
|
+
"mode",
|
|
11721
|
+
"created_at",
|
|
11722
|
+
"updated_at"
|
|
11723
|
+
],
|
|
11724
|
+
additionalProperties: true
|
|
11725
|
+
},
|
|
11726
|
+
CustomerDetail: {
|
|
11727
|
+
type: "object",
|
|
11728
|
+
properties: {
|
|
11729
|
+
customer: {
|
|
11730
|
+
$ref: "#/components/schemas/Customer"
|
|
11731
|
+
},
|
|
11732
|
+
recent_payments: {
|
|
11733
|
+
type: "array",
|
|
11734
|
+
items: {
|
|
11735
|
+
type: "object",
|
|
11736
|
+
additionalProperties: true
|
|
11737
|
+
},
|
|
11738
|
+
description: "Up to 20 most recent checkout sessions paid by this customer, newest first (session objects)."
|
|
11739
|
+
}
|
|
11740
|
+
},
|
|
11741
|
+
required: [
|
|
11742
|
+
"customer",
|
|
11743
|
+
"recent_payments"
|
|
11744
|
+
]
|
|
11745
|
+
},
|
|
11041
11746
|
DepositAddress: {
|
|
11042
11747
|
type: "object",
|
|
11043
11748
|
properties: {
|
|
@@ -11214,7 +11919,7 @@ var docs_data_default = {
|
|
|
11214
11919
|
"integer",
|
|
11215
11920
|
"null"
|
|
11216
11921
|
],
|
|
11217
|
-
description: "Decimals of the payment token (USDC/USDT = 6
|
|
11922
|
+
description: "Decimals of the payment token (most USDC/USDT = 6; BNB Chain USDT/USDC and JPYC = 18). Null before a deposit address was issued."
|
|
11218
11923
|
},
|
|
11219
11924
|
fee_amount_token: {
|
|
11220
11925
|
type: [
|
|
@@ -11527,7 +12232,7 @@ var docs_data_default = {
|
|
|
11527
12232
|
"integer",
|
|
11528
12233
|
"null"
|
|
11529
12234
|
],
|
|
11530
|
-
description: "Decimals of the payment token (USDC/USDT = 6
|
|
12235
|
+
description: "Decimals of the payment token (most USDC/USDT = 6; BNB Chain USDT/USDC and JPYC = 18). Null before a deposit address was issued."
|
|
11531
12236
|
},
|
|
11532
12237
|
fee_amount_token: {
|
|
11533
12238
|
type: [
|
|
@@ -11645,7 +12350,7 @@ var docs_data_default = {
|
|
|
11645
12350
|
"integer",
|
|
11646
12351
|
"null"
|
|
11647
12352
|
],
|
|
11648
|
-
description: "Decimals of the payment token (USDC/USDT = 6
|
|
12353
|
+
description: "Decimals of the payment token (most USDC/USDT = 6; BNB Chain USDT/USDC and JPYC = 18). Null before a deposit address was issued."
|
|
11649
12354
|
},
|
|
11650
12355
|
fee_amount_token: {
|
|
11651
12356
|
type: [
|
|
@@ -12745,6 +13450,45 @@ var docs_data_default = {
|
|
|
12745
13450
|
],
|
|
12746
13451
|
additionalProperties: false
|
|
12747
13452
|
},
|
|
13453
|
+
UpdateCustomerRequest: {
|
|
13454
|
+
type: "object",
|
|
13455
|
+
properties: {
|
|
13456
|
+
email: {
|
|
13457
|
+
type: [
|
|
13458
|
+
"string",
|
|
13459
|
+
"null"
|
|
13460
|
+
],
|
|
13461
|
+
format: "email",
|
|
13462
|
+
maxLength: 320
|
|
13463
|
+
},
|
|
13464
|
+
name: {
|
|
13465
|
+
type: [
|
|
13466
|
+
"string",
|
|
13467
|
+
"null"
|
|
13468
|
+
],
|
|
13469
|
+
maxLength: 200
|
|
13470
|
+
},
|
|
13471
|
+
phone: {
|
|
13472
|
+
type: [
|
|
13473
|
+
"string",
|
|
13474
|
+
"null"
|
|
13475
|
+
],
|
|
13476
|
+
maxLength: 50
|
|
13477
|
+
},
|
|
13478
|
+
country: {
|
|
13479
|
+
type: [
|
|
13480
|
+
"string",
|
|
13481
|
+
"null"
|
|
13482
|
+
],
|
|
13483
|
+
maxLength: 100
|
|
13484
|
+
},
|
|
13485
|
+
metadata: {
|
|
13486
|
+
type: "object",
|
|
13487
|
+
additionalProperties: true
|
|
13488
|
+
}
|
|
13489
|
+
},
|
|
13490
|
+
additionalProperties: false
|
|
13491
|
+
},
|
|
12748
13492
|
UpdatePaymentLinkRequest: {
|
|
12749
13493
|
type: "object",
|
|
12750
13494
|
properties: {
|
|
@@ -14241,7 +14985,7 @@ var actionTools = [
|
|
|
14241
14985
|
}),
|
|
14242
14986
|
tool({
|
|
14243
14987
|
name: "simulate_payment",
|
|
14244
|
-
description: `Simulate the buyer's deposit for a TEST-mode session (testnet tokens are minted to the deposit address): exact (pay in full), underpay (half), overpay (+1 token, excess refunded) or wrong_token. The buyer refund address defaults to BitPal's test buyer wallet (not your payout wallet), so refunds go back to a "buyer". Requires a bp_test_ API key.`,
|
|
14988
|
+
description: `Simulate the buyer's deposit for a TEST-mode session (testnet tokens are minted to the deposit address): exact (pay in full), underpay (half), overpay (+1 token, excess refunded) or wrong_token. Issues the deposit address on first use \u2014 a session that expired without one cannot be paid. Late payment: on a session whose address was issued and that then expired unpaid, exact/overpay sends a late payment (late_payments accept \u2192 paid with late=true; refund \u2192 unresolved reason=late, refunded ~7 days after the deadline). wrong_token only works on testnets with a second test token (the error lists them, e.g. BSC Testnet, Polygon Amoy). The buyer refund address defaults to BitPal's test buyer wallet (not your payout wallet), so refunds go back to a "buyer". Requires a bp_test_ API key.`,
|
|
14245
14989
|
inputSchema: {
|
|
14246
14990
|
type: "object",
|
|
14247
14991
|
properties: {
|
|
@@ -14426,7 +15170,7 @@ async function checkTestKey(apiKey, apiBase = DEFAULT_API_BASE, now = Date.now()
|
|
|
14426
15170
|
|
|
14427
15171
|
// src/protocol.ts
|
|
14428
15172
|
var SERVER_NAME = "bitpal";
|
|
14429
|
-
var SERVER_VERSION = "0.1.
|
|
15173
|
+
var SERVER_VERSION = "0.1.5";
|
|
14430
15174
|
var SUPPORTED_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
14431
15175
|
var INSTRUCTIONS = [
|
|
14432
15176
|
"BitPal is a non-custodial stablecoin checkout. Use these tools to write correct BitPal integrations:",
|