Reference
API reference
Every HTTP endpoint under /api/v1, with request schemas, response objects and status codes as implemented.
The API is small on purpose. Your server creates checkout sessions and refund requests and reads payments. The checkout widget reads a public view of a session and reports the transaction hash it sent. Money never moves through the API: payments go from the customer's wallet to your payout wallet through the PaymentRouter contract, and refunds are signed by your payout wallet.
Base URL
The hosted API base URL is:
https://whitechain-dashboard.vercel.app/api/v1
Use the API origin shown under Developers in your dashboard. For hosted checkout, redirect to the URL returned in CheckoutSession.url.
| Resource | Value |
|---|---|
| Chain ID | 1874 (0x752) |
| PaymentRouter | 0xa99a6D97607567f4fe734831910D24f4e9BD0508 |
| USDT | 0xcedfB56113143d55b2358b8CFbB0dEB5Ad2f55a7, 6 decimals |
| Explorer | https://explorer.testnet.whitechain.io |
| Public RPC | https://rpc.testnet.whitechain.io |
Authentication
Authenticated endpoints take a secret key in the Authorization header:
Authorization: Bearer sk_test_...
Create and revoke keys in the dashboard under Developers. The full key is shown once; the server stores only its SHA-256 hash.
A request is rejected with 401 authentication when:
- the header is missing or not of the form
Bearer sk_test_<letters and digits>, - the key does not exist or has been revoked,
- the merchant account that owns the key is not
active.
Two endpoints are public because the checkout widget calls them from the customer's browser:
| Endpoint | Auth |
|---|---|
POST /api/v1/checkout_sessions |
Secret key |
GET /api/v1/checkout_sessions/:id |
None (public) |
POST /api/v1/checkout_sessions/:id/submit |
None (public) |
GET /api/v1/payments |
Secret key |
GET /api/v1/payments/:id |
Secret key |
POST /api/v1/refunds |
Secret key |
GET /api/v1/refunds |
Secret key |
The public endpoints return no secrets: no metadata, no keys, and no other sessions or payments. Anyone who knows a session id can read its public view and post a transaction hash hint, which is why a hint never marks a session paid on its own (see Checkout sessions).
Keep secret keys on your server. CORS is open (below), so the API would accept a key sent from a browser, but anyone could then read it from your page.
CORS
Every /api/v1 response, including errors, carries:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,POST,OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Every /api/v1 route answers OPTIONS preflight requests with 204 No Content, the headers above and Access-Control-Max-Age: 86400.
Requests and responses
- Send request bodies as JSON. The body is parsed as JSON whatever the
Content-Type, but sendContent-Type: application/jsonanyway. - A body that is not valid JSON returns
400 invalid_requestwith the messageRequest body must be valid JSON. - Responses are JSON. Errors use one envelope, described in Errors:
{ "error": { "type": "invalid_request", "message": "amount: must be a decimal string like \"49.99\"" } }
- Amounts are decimal strings in whole USDT, for example
"49.99". They are never numbers, so no precision is lost. See Amounts. - Timestamps are ISO 8601 strings in UTC, for example
"2026-11-06T10:15:00.000Z". - Onchain values (addresses, hashes, ids) are
0x-prefixed hex strings. Transaction hashes,chainMerchantId,chainOrderIdandchainPaymentIdare lowercase. Addresses are returned as stored, so compare them case-insensitively.
Idempotency
There is no Idempotency-Key header. Idempotency is built into the resources instead:
- Checkout sessions are idempotent per
orderId. Creating a session again for the sameorderIdand the same amount, while the earlier session is stillprocessingoropenand unexpired, returns that session with200 OKinstead of201 Created. Concurrent creates for the same order are serialized, so retries cannot produce two payable sessions for the same order and amount. Details in Checkout sessions. - Payments are created only by the indexer, at most once per session.
- Refunds are not idempotent. Each
POST /api/v1/refundscreates a new pending refund and reserves its amount. Do not blindly retry a refund request that may have succeeded; list the payment's refunds first.
Pagination and limits
There is no cursor pagination. List endpoints return the newest items first, up to a limit.
| Endpoint | Limit |
|---|---|
GET /api/v1/payments |
limit query parameter, integer from 1 to 100, default 20 |
GET /api/v1/refunds |
Fixed at 100, not configurable |
For longer history, the dashboard can export payments and succeeded refunds as CSV.
There is no rate limiting on the API itself.
Create a checkout session
POST /api/v1/checkout_sessions
Auth: secret key.
Creates a checkout session for one of your orders. Call this from your server, so the customer cannot change the amount. Sessions expire 30 minutes after creation.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
orderId |
string | Yes | Your own order id. Leading and trailing whitespace is trimmed. 1 to 200 characters after trimming. |
amount |
string | Yes | Decimal USDT amount, matching ^\d+(\.\d+)?$, at most 40 characters. Must be greater than zero and have at most 6 decimal places (the token's onchain decimals()). |
currency |
string | No | Only "USDT" is accepted. |
successUrl |
string or null | No | Where the hosted checkout sends the customer after a confirmed payment. Must be an http or https URL, at most 2048 characters. |
cancelUrl |
string or null | No | Where the widget's Cancel button and the expired screen send the customer. Same rules as successUrl. |
metadata |
object or null | No | String-to-string map stored with the session. Keys at most 40 characters, values at most 500 characters, at most 20 keys. Values must be strings. |
The merchant account must have a payout wallet registered onchain. Otherwise the request fails with 409 conflict (see Errors).
Response
201 Created with a CheckoutSession when a new session was created.
200 OK with the existing CheckoutSession when an equivalent session already exists for this orderId (same amount, still processing or open and unexpired).
Status codes
| Status | Type | When |
|---|---|---|
| 201 | New session created | |
| 200 | Existing session returned (idempotent re-create) | |
| 400 | invalid_request |
Body is not JSON, fails validation, amount is zero or has too many decimals |
| 401 | authentication |
Missing, invalid or revoked key, or inactive merchant |
| 409 | conflict |
No payout wallet registered, order already paid, or another payment for the order is being processed |
| 500 | server_error |
Unexpected error |
Example
curl -X POST https://whitechain-dashboard.vercel.app/api/v1/checkout_sessions \
-H "Authorization: Bearer $WC_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"orderId": "order_1042",
"amount": "49.99",
"currency": "USDT",
"successUrl": "https://shop.example.com/orders/order_1042",
"cancelUrl": "https://shop.example.com/cart",
"metadata": { "customerId": "cus_88" }
}'
{
"id": "cs_Q3n8ZpK2vRt6YxLm4WbH",
"status": "open",
"merchant": { "name": "Chai & Co." },
"orderId": "order_1042",
"amount": "49.99",
"amountUnits": "49990000",
"currency": "USDT",
"decimals": 6,
"chainId": 1874,
"routerAddress": "0xa99a6D97607567f4fe734831910D24f4e9BD0508",
"tokenAddress": "0xcedfB56113143d55b2358b8CFbB0dEB5Ad2f55a7",
"chainMerchantId": "0xa65fb120a3b9e9a9ffbfff63fa7b84676fbc256b68a2448e22f5aad57e83d6e1",
"chainOrderId": "0x4e03aae7bd5a2c096c69ac1a4f01de6920f219fdf1ac89caec7efc539937f2ff",
"expiresAt": "2026-11-06T10:45:00.000Z",
"successUrl": "https://shop.example.com/orders/order_1042",
"cancelUrl": "https://shop.example.com/cart",
"payment": null,
"url": "https://whitechain-dashboard.vercel.app/checkout/cs_Q3n8ZpK2vRt6YxLm4WbH",
"metadata": { "customerId": "cus_88" },
"createdAt": "2026-11-06T10:15:00.000Z"
}
From TypeScript, the server helper in the React SDK wraps this endpoint:
import { createCheckoutSession } from "@whitechain-commerce/react/server";
const session = await createCheckoutSession({
apiBase: "https://whitechain-dashboard.vercel.app",
secretKey: process.env.WC_SECRET_KEY!,
orderId: "order_1042",
amount: "49.99",
successUrl: "https://shop.example.com/orders/order_1042",
cancelUrl: "https://shop.example.com/cart",
metadata: { customerId: "cus_88" },
});
// session.id -> pass to <WhitechainCheckout sessionId={...} />, or redirect to session.url
apiBase falls back to NEXT_PUBLIC_WC_API_BASE when omitted. The helper always sends currency: "USDT" and checks a few things before calling the API (key starts with sk_, orderId is set, amount is a positive decimal with at most 6 decimals, an API base is known). Failures throw WhitechainApiError with status and type; see Errors.
Retrieve a checkout session (public)
GET /api/v1/checkout_sessions/:id
Auth: none.
Returns the public view of a session. The checkout widget polls this every 2 seconds while it waits for confirmation. If the session is open, has no payment and its expiresAt has passed, this call marks it expired before answering.
There is no authenticated "retrieve session" endpoint. The full CheckoutSession (with url, metadata and createdAt) is returned only by the create call. To learn that an order was paid, listen for the payment.succeeded webhook or list payments.
Status codes
| Status | Type | When |
|---|---|---|
| 200 | Session found | |
| 404 | not_found |
No session with that id |
| 500 | server_error |
Unexpected error |
Example
curl https://whitechain-dashboard.vercel.app/api/v1/checkout_sessions/cs_Q3n8ZpK2vRt6YxLm4WbH
{
"id": "cs_Q3n8ZpK2vRt6YxLm4WbH",
"status": "paid",
"merchant": { "name": "Chai & Co." },
"orderId": "order_1042",
"amount": "49.99",
"amountUnits": "49990000",
"currency": "USDT",
"decimals": 6,
"chainId": 1874,
"routerAddress": "0xa99a6D97607567f4fe734831910D24f4e9BD0508",
"tokenAddress": "0xcedfB56113143d55b2358b8CFbB0dEB5Ad2f55a7",
"chainMerchantId": "0xa65fb120a3b9e9a9ffbfff63fa7b84676fbc256b68a2448e22f5aad57e83d6e1",
"chainOrderId": "0x4e03aae7bd5a2c096c69ac1a4f01de6920f219fdf1ac89caec7efc539937f2ff",
"expiresAt": "2026-11-06T10:45:00.000Z",
"successUrl": "https://shop.example.com/orders/order_1042",
"cancelUrl": "https://shop.example.com/cart",
"payment": {
"id": "pay_Vb3kT9wQ2mXr7LpN5sZe",
"txHash": "0xca1467eb1d7b29899ff948051bd8cd21e1c4581e9e60d6d9cbc0ccf74e97e9b4",
"explorerUrl": "https://explorer.testnet.whitechain.io/tx/0xca1467eb1d7b29899ff948051bd8cd21e1c4581e9e60d6d9cbc0ccf74e97e9b4"
}
}
Submit a transaction hash (public)
POST /api/v1/checkout_sessions/:id/submit
Auth: none.
The checkout widget calls this after the customer's pay transaction is broadcast. It is a hint that speeds up confirmation. It never marks a session paid by itself: only a payment that passes the indexer's checks does that (see Payments).
What it does:
- If the session is already
paid, it changes nothing and returnspaid. - Otherwise it stores the hash (replacing any earlier hint) and moves an
opensession toprocessing.processingandexpiredsessions keep their status. - It runs the indexer's validation on that transaction and waits up to 6 seconds for it. If the transaction is already confirmed and valid, the session is
paidby the time the response is sent. - It returns the session's current status.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
txHash |
string | Yes | 0x followed by 64 hex characters. Stored in lowercase. |
Response
200 OK:
| Field | Type | Description |
|---|---|---|
ok |
true |
Always true on success. |
status |
string | The session status after the call: open, processing, paid or expired. |
Status codes
| Status | Type | When |
|---|---|---|
| 200 | Hint stored (or session already paid) | |
| 400 | invalid_request |
Body is not JSON or txHash is malformed |
| 404 | not_found |
No session with that id |
| 500 | server_error |
Unexpected error |
Example
curl -X POST https://whitechain-dashboard.vercel.app/api/v1/checkout_sessions/cs_Q3n8ZpK2vRt6YxLm4WbH/submit \
-H "Content-Type: application/json" \
-d '{ "txHash": "0xca1467eb1d7b29899ff948051bd8cd21e1c4581e9e60d6d9cbc0ccf74e97e9b4" }'
{ "ok": true, "status": "processing" }
List payments
GET /api/v1/payments
Auth: secret key.
Returns your payments, newest first.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
limit |
integer | No | 1 to 100. Default 20. Anything else (including an empty value or a decimal) returns 400. |
Response
200 OK:
| Field | Type | Description |
|---|---|---|
data |
PaymentObject[] | Payments ordered by createdAt descending. |
Status codes
| Status | Type | When |
|---|---|---|
| 200 | Success, possibly an empty list | |
| 400 | invalid_request |
limit must be an integer between 1 and 100. |
| 401 | authentication |
Missing, invalid or revoked key, or inactive merchant |
| 500 | server_error |
Unexpected error |
Example
curl "https://whitechain-dashboard.vercel.app/api/v1/payments?limit=2" \
-H "Authorization: Bearer $WC_SECRET_KEY"
{
"data": [
{
"id": "pay_Vb3kT9wQ2mXr7LpN5sZe",
"sessionId": "cs_Q3n8ZpK2vRt6YxLm4WbH",
"orderId": "order_1042",
"amount": "49.99",
"amountRefunded": "10.00",
"currency": "USDT",
"status": "partially_refunded",
"payer": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"txHash": "0xca1467eb1d7b29899ff948051bd8cd21e1c4581e9e60d6d9cbc0ccf74e97e9b4",
"explorerUrl": "https://explorer.testnet.whitechain.io/tx/0xca1467eb1d7b29899ff948051bd8cd21e1c4581e9e60d6d9cbc0ccf74e97e9b4",
"chainId": 1874,
"chainPaymentId": "0x88dc5926bd868fe33725c7369d5db8d07bb46f80a0a68f78a70dfa0f6ecf40b1",
"createdAt": "2026-11-06T10:16:12.000Z"
}
]
}
Retrieve a payment
GET /api/v1/payments/:id
Auth: secret key.
Returns one PaymentObject. Payments of other merchants return 404, the same as ids that do not exist.
Status codes
| Status | Type | When |
|---|---|---|
| 200 | Payment found | |
| 401 | authentication |
Missing, invalid or revoked key, or inactive merchant |
| 404 | not_found |
No payment <id>. |
| 500 | server_error |
Unexpected error |
Example
curl https://whitechain-dashboard.vercel.app/api/v1/payments/pay_Vb3kT9wQ2mXr7LpN5sZe \
-H "Authorization: Bearer $WC_SECRET_KEY"
The response is a single PaymentObject, as in the list example.
Create a refund
POST /api/v1/refunds
Auth: secret key.
Creates a pending refund request for a payment. The API cannot move funds. The refund completes when you sign it from your payout wallet in the dashboard and the indexer sees the onchain Refunded event. See Refunds.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
paymentId |
string | Yes | A pay_... id that belongs to you. Trimmed, 1 to 100 characters. |
amount |
string | Yes | Decimal USDT amount, matching ^\d+(\.\d+)?$, at most 40 characters. Greater than zero, at most 6 decimal places, and no more than the refundable balance. |
The refundable balance is the payment amount minus what has already been refunded onchain minus every other pending refund for the payment.
Response
201 Created with a RefundObject whose status is pending and txHash is null.
Status codes
| Status | Type | When |
|---|---|---|
| 201 | Pending refund created | |
| 400 | invalid_request |
Body is not JSON, fails validation, amount is zero, has too many decimals, or exceeds the refundable balance |
| 401 | authentication |
Missing, invalid or revoked key, or inactive merchant |
| 404 | not_found |
No payment <paymentId>. |
| 500 | server_error |
Unexpected error |
Example
curl -X POST https://whitechain-dashboard.vercel.app/api/v1/refunds \
-H "Authorization: Bearer $WC_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "paymentId": "pay_Vb3kT9wQ2mXr7LpN5sZe", "amount": "10.00" }'
{
"id": "re_Jt6pW2nR8vKx3QmY9cLa",
"paymentId": "pay_Vb3kT9wQ2mXr7LpN5sZe",
"orderId": "order_1042",
"amount": "10.00",
"currency": "USDT",
"status": "pending",
"txHash": null,
"explorerUrl": null,
"createdAt": "2026-11-06T11:02:40.000Z"
}
List refunds
GET /api/v1/refunds
Auth: secret key.
Returns your refunds, newest first, at most 100.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
paymentId |
string | No | Only refunds of this payment. An unknown id returns an empty list, not 404. An empty value (?paymentId=) is ignored and returns all your refunds. |
Response
200 OK:
| Field | Type | Description |
|---|---|---|
data |
RefundObject[] | Refunds ordered by createdAt descending. Includes pending and succeeded refunds. Discarded pending refunds are deleted and do not appear. |
Status codes
| Status | Type | When |
|---|---|---|
| 200 | Success, possibly an empty list | |
| 401 | authentication |
Missing, invalid or revoked key, or inactive merchant |
| 500 | server_error |
Unexpected error |
Example
curl "https://whitechain-dashboard.vercel.app/api/v1/refunds?paymentId=pay_Vb3kT9wQ2mXr7LpN5sZe" \
-H "Authorization: Bearer $WC_SECRET_KEY"
{
"data": [
{
"id": "re_Jt6pW2nR8vKx3QmY9cLa",
"paymentId": "pay_Vb3kT9wQ2mXr7LpN5sZe",
"orderId": "order_1042",
"amount": "10.00",
"currency": "USDT",
"status": "succeeded",
"txHash": "0x4104654031bd074fbae534380065dad4116c4d62f706858cf10491af9e635d29",
"explorerUrl": "https://explorer.testnet.whitechain.io/tx/0x4104654031bd074fbae534380065dad4116c4d62f706858cf10491af9e635d29",
"createdAt": "2026-11-06T11:02:40.000Z"
}
]
}
Objects
Types come from @whitechain-commerce/core (packages/core/src/types.ts and packages/core/src/webhooks.ts).
PublicCheckoutSession
Returned by GET /api/v1/checkout_sessions/:id. Safe to show in a browser.
| Field | Type | Description |
|---|---|---|
id |
string | Session id, cs_ followed by 20 letters and digits. |
status |
"open" | "processing" | "paid" | "expired" |
See Session lifecycle. |
merchant |
{ name: string } |
Your business name as set in the dashboard. |
orderId |
string | Your order id, as sent (trimmed). |
amount |
string | Normalized decimal amount with at least 2 decimals, for example "5.00" for an input of "5". |
amountUnits |
string | Amount in token base units as an integer string, for example "49990000" for 49.99 USDT. This is the exact value the customer must pay. |
currency |
"USDT" |
Always USDT. |
decimals |
number | Token decimals used to price the session, read onchain at creation. 6 for USDT. |
chainId |
1874 |
Settlement chain ID. |
routerAddress |
string | PaymentRouter address the customer pays through. |
tokenAddress |
string | USDT token address. |
chainMerchantId |
string | keccak256 of your merchant id (UTF-8), the merchantId argument of pay(). |
chainOrderId |
string | keccak256 of the session id (UTF-8), the orderId argument of pay(). One session maps to one onchain order. |
expiresAt |
string | ISO timestamp, 30 minutes after creation. |
successUrl |
string | null | As set on creation. |
cancelUrl |
string | null | As set on creation. |
payment |
object | null | null until a payment is recorded. Then { id, txHash, explorerUrl } of that payment. |
CheckoutSession
Returned by POST /api/v1/checkout_sessions. All fields of PublicCheckoutSession, plus:
| Field | Type | Description |
|---|---|---|
url |
string | Full URL to the hosted checkout page. |
metadata |
object | null | Your metadata, or null. |
createdAt |
string | ISO timestamp of creation. |
On the create response, payment is always null.
PaymentObject
Returned by the payments endpoints and inside webhook events.
| Field | Type | Description |
|---|---|---|
id |
string | Payment id, pay_ followed by 20 letters and digits. |
sessionId |
string | The checkout session it paid. |
orderId |
string | Your order id from that session. |
amount |
string | Amount paid, decimal string with at least 2 decimals. Always equal to the session amount. |
amountRefunded |
string | Total refunded onchain so far, for example "0.00". Pending refunds are not included. |
currency |
"USDT" |
Always USDT. |
status |
"succeeded" | "partially_refunded" | "refunded" |
succeeded until the first refund, partially_refunded while amountRefunded is above zero and below amount, refunded once it reaches amount. |
payer |
string | Address that sent the payment. Refunds go back to this address. |
txHash |
string | Transaction that emitted PaymentSettled, lowercase. |
explorerUrl |
string | https://explorer.testnet.whitechain.io/tx/<txHash>. |
chainId |
1874 |
Settlement chain ID. |
chainPaymentId |
string | Onchain payment id, keccak256(abi.encode(chainMerchantId, chainOrderId)), lowercase. The first argument of refund(). |
createdAt |
string | ISO timestamp of when the payment was recorded by the indexer (not the block time). |
RefundObject
Returned by the refunds endpoints and inside refund.succeeded events.
| Field | Type | Description |
|---|---|---|
id |
string | Refund id, re_ followed by 20 letters and digits. |
paymentId |
string | The refunded payment. |
orderId |
string | Your order id. |
amount |
string | Refund amount, decimal string with at least 2 decimals. |
currency |
"USDT" |
Always USDT. |
status |
"pending" | "succeeded" |
pending until the onchain refund is indexed, then succeeded. |
txHash |
string | null | Refund transaction, lowercase. null while pending. |
explorerUrl |
string | null | Explorer link for txHash, or null. |
createdAt |
string | ISO timestamp. For a request made through the API or dashboard, when the request was created. For a refund made without a request, when the indexer recorded it. |
Not available
These do not exist in the API today: an authenticated session retrieve or list endpoint, session cancel or expire endpoints, refund retrieve by id, refund cancel through the API (discard pending refunds in the dashboard), payment links, subscriptions, and webhook endpoint management through the API (use the dashboard).