API Reference
The SDK covers order creation. Everything else you may want from your backend, such as looking up an order or listing settled payments, is a plain HTTP call against the same API. This section documents those endpoints.
Base URL
https://api.pay.peer.xyz
Every path on these pages is relative to that host and lives under /api/v1.
Authentication
Merchant endpoints take your API key in the X-API-Key header. The key lives under
Settings, Developer in the merchant dashboard; see
API keys and webhooks for rotation rules.
curl https://api.pay.peer.xyz/api/v1/merchants/me/orders \
-H "X-API-Key: $ZKPAY_API_KEY"
An API key reads and creates orders for your whole account. Send it only from a trusted backend, never from browser or mobile code.
Sandbox and live are separate accounts with separate keys. A sandbox key sees only sandbox
orders and payments, and a live key sees only live ones. A missing or unknown key returns
401 with the message Missing API key or Invalid API key.
One endpoint, get an order by id, takes no key at all. It is the read the hosted checkout page uses, and the order id is the only credential.
IP allowlist
Owners can restrict an API key to specific source IPs under Settings, Developer, API key IP allowlist.
Entries are public IPv4/IPv6 addresses or CIDR blocks (IPv4 prefix 8 to 32, IPv6 prefix 32 to 128, up to 50
entries). A bare address is stored as /32 or /128. Live and sandbox keys have separate lists. An
empty list turns the allowlist off.
When a request arrives from an address outside the list, the API responds 403:
{
"success": false,
"message": "IP address 203.0.114.7 is not in this merchant's API key IP allowlist",
"responseObject": { "ip": "203.0.114.7", "reason": "IP_NOT_ALLOWED" },
"statusCode": 403,
"errorCode": "IP_NOT_ALLOWED"
}
reason is IP_NOT_ALLOWED when the caller's address was resolved but is not listed, and
CLIENT_IP_UNRESOLVED (message Client IP could not be resolved for API key IP allowlist enforcement)
when the connecting address could not be established, for example a request that did not arrive
through the public ingress. The ip field is the address to add if the caller is yours. Dashboard
sessions never use the API key and are unaffected. Peer-operated integrations (currently the Telegram
bot) are exempt from merchant allowlists.
Response envelope
Every response, success or failure, is a JSON object with the same four fields:
{
"success": true,
"message": "Merchant orders retrieved",
"responseObject": { "...": "..." },
"statusCode": 200
}
| Field | Type | Description |
|---|---|---|
success | boolean | true for a 2xx response |
message | string | Written for a log line, not for matching |
responseObject | object | null | The payload. null on most failures |
statusCode | number | Mirrors the HTTP status |
In TypeScript, one type covers every response on these pages. The examples reuse it:
type Envelope<T> = { success: boolean; message: string; responseObject: T | null; statusCode: number };
A 400 from query or body validation carries the validation detail in responseObject:
{
"success": false,
"message": "Invalid request",
"responseObject": {
"formErrors": [],
"fieldErrors": { "limit": ["Number must be less than or equal to 100"] }
},
"statusCode": 400
}
Some failures also carry a top-level errorCode string you can branch on. The order
creation codes, such as AMOUNT_ABOVE_MAX for an order above the 10,000 USDC
order maximum, are listed under
createCheckout errors. Starting a payment can also
return INTENT_ABOVE_MAX when a bank or app payment would settle more than
10,000 USDC once buyer-paid fees are added.
Common errors
Three failures behave the same on every endpoint on these pages, so the endpoint pages list only what is specific to them:
| Status | Cause |
|---|---|
400 | A path parameter, query parameter, or body failed validation. Detail in responseObject.fieldErrors |
401 | Missing or invalid API key. Never returned by the keyless order read |
429 | Rate limit exceeded |
Pagination
The order and payment lists page with two query parameters and report the total (list payments for an order returns every row for one order and takes neither):
| Parameter | Default | Max | Description |
|---|---|---|---|
page | 1 | none | 1-based page number |
limit | 20 | 100 | Rows per page |
The payload carries the rows, total matching rows across all pages, and the page and
limit that were applied. Rows are always newest first. A limit above 100 is rejected
with 400, not clamped.
Dates and amounts
Timestamps are ISO 8601 strings in UTC, for example 2026-09-03T10:15:42.318Z. Amounts are
decimal strings, never floats: "25" or "12.5" for USDC fields and "23.14" for fiat
fields. Trailing zeros are not preserved, so parse rather than string-compare.
Chain ids are decimal strings too, so Base is "8453".
Rate limits
The API allows 1200 requests per minute per source IP across all endpoints. Past that it
answers 429 with the message Too many requests and three headers:
| Header | Value |
|---|---|
x-retry-after | Seconds until you may retry |
x-rate-limit-remaining | Requests left in the current window |
x-rate-limiter-resets-at | ISO 8601 time the window resets |
Remediation endpoints add a per-merchant limit (30
actions and 120 reads per minute) with the same 429 shape.
There is no standard Retry-After header. Webhook deliveries are what tell you an order
changed; poll these endpoints for reconciliation, not as your primary signal.
Endpoints
| Endpoint | Auth | Documented at |
|---|---|---|
POST /api/v1/orders | API key | createCheckout; open-amount orders |
GET /api/v1/merchants/me | API key | getMerchant |
GET /api/v1/merchants/me/tier-usage | API key | Watching your usage |
POST /api/v1/merchants/me/quotes/availability | API key | checkQuoteAvailability |
GET /api/v1/merchants/me/orders | API key | List orders |
GET /api/v1/orders/{orderId} | None | Get an order by id |
GET /api/v1/merchants/me/payments | API key | List payments |
GET /api/v1/merchants/me/orders/{orderId}/payments | API key | List payments for an order |
POST /api/v1/merchants/me/orders/{orderId}/payments/{paymentId}/recreate | API key | Recreate |
POST /api/v1/merchants/me/orders/{orderId}/payments/{paymentId}/extend | API key | Extend |
POST /api/v1/merchants/me/orders/{orderId}/payments/{paymentId}/fulfill-sar | API key | Fulfill from a transaction id |
GET /api/v1/merchants/me/orders/{orderId}/actions/{actionId} | API key | Poll an action |
POST /api/v1/webhooks | API key | Create a webhook |
POST /api/v1/sandbox/test-order | Sandbox key | Let a coding agent integrate |
GET /api/v1/integration/status | API key | Let a coding agent integrate |
Cancelling an order and reading a single order through the merchant-scoped path need a dashboard session, so they are not available to an API key. Cancel from the dashboard.