Orders
An order is the thing you create with createCheckout. This page
covers reading it back. There are two reads:
| Endpoint | Auth | Use it when |
|---|---|---|
GET /api/v1/merchants/me/orders | API key | You are on your backend and want one order by id, or a filtered page of orders |
GET /api/v1/orders/{orderId} | None | You hold an order id and want the hosted checkout view, including the customer's current payment attempt |
Both return the CheckoutOrder shape. Webhooks remain the
signal that an order changed; use these reads to reconcile or to answer a support question.
Order status
status has four values. It tracks how much of the order's value has settled, not where the
customer is in the payment flow.
| Status | Meaning |
|---|---|
CREATED | Open. Nothing has settled yet. The customer may not have started, may be mid-payment, or may have let an attempt expire |
PARTIALLY_FULFILLED | Some value has settled and remainingUsdcAmount is what is still owed. A settlement covered less than the order, for example a partial fill on a crypto rail, and the customer can pay the remainder in a new attempt |
FULFILLED | Settled. remainingUsdcAmount is "0", or a dust residual under the completion threshold (0.5 USDC by default) that no further payment could cover, and completedAt is set |
CANCELLED | Cancelled from the dashboard. cancelledAt is set |
An order never expires on its own. The customer window (one hour on fiat rails, about six
hours on crypto rails) expires the payment attempt, which fires PAYMENT_EXPIRED on fiat
rails (PAYMENT_FAILED on live crypto rails) and leaves the order CREATED so a late
settlement can still fulfil it. Where the customer is right now lives on the payment, which the
hosted read returns as currentPayment and the
Payments API lists, across orders or
for one order.
Chargebacks and refunds are additive facts beside status, carried in chargebackStatus,
chargebacks, and the refund* fields. They never move status off FULFILLED. See
chargeback status.
Attestation penalties are per-payment facts carried in currentPayment.penalties (and on
every payment the Payments API returns). See
payment penalties.
List orders
GET /api/v1/merchants/me/orders
X-API-Key: <your key>
Returns your orders, newest first, with optional filters. Pass the full order id in
orderId to fetch one order.
Query parameters
| Parameter | Type | Description |
|---|---|---|
orderId | string | Case-insensitive substring match on the order id. A full id matches exactly one order. Ignored when search is set |
status | CheckoutOrderStatus | CREATED, PARTIALLY_FULFILLED, FULFILLED, or CANCELLED. Ignored when displayStatus is set |
displayStatus | string | Slices open orders by payment activity. See display status |
chargebackStatus | OrderChargebackStatus | NONE, PARTIALLY_CHARGEBACKED, or CHARGEBACKED |
search | string | Free-text search, at least 3 characters. Shorter values are ignored. See search |
page | number | 1-based page, default 1 |
limit | number | Rows per page, default 20, max 100 |
Display status
displayStatus answers "what is the customer doing on this open order". Every value implies
status=CREATED, so combining it with status is redundant and status is dropped.
| Value | Matches an open order that |
|---|---|
ACTIVE | Has a payment attempt in progress, so a customer is inside their payment window |
EXPIRED | Has at least one expired attempt and none in progress. The customer walked away |
CREATED | Has no attempt in progress and none expired. Either the customer never picked a payment method, or every attempt was cancelled or failed |
Search
search runs a case-insensitive substring match across the order id, the notes and
metadata JSON you attached at creation, the payment rail, and the payment's
railIdentifier. Searching for your own
order reference works when you put it in notes, as the
recommended backend pattern does.
Response
responseObject is a page:
type MerchantOrderList = {
orders: MerchantOrderListItem[];
total: number;
page: number;
limit: number;
};
type MerchantOrderListItem = CheckoutOrder & {
hasActivePayment: boolean;
hasExpiredPayment: boolean;
paymentSelectedRail: string | null;
cryptoReferralSplitConfig: ReferralSplitConfig | null;
};
| Extra field | Description |
|---|---|
hasActivePayment | A payment attempt is in progress |
hasExpiredPayment | At least one attempt expired |
paymentSelectedRail | Rail of the attempt in progress, else of the most recent expired one, else null |
cryptoReferralSplitConfig | Referral split that applies when the order is paid over a crypto rail |
List rows do not carry requestedFiat or bridgeInfo. Those come from the
hosted read only.
Example
curl "https://api.pay.peer.xyz/api/v1/merchants/me/orders?orderId=cmf5k2x9d0001abcd1234efgh" \
-H "X-API-Key: $ZKPAY_API_KEY"
type Envelope<T> = { success: boolean; message: string; responseObject: T | null; statusCode: number };
async function getOrder(orderId: string) {
const url = new URL('https://api.pay.peer.xyz/api/v1/merchants/me/orders');
url.searchParams.set('orderId', orderId);
url.searchParams.set('limit', '1');
const response = await fetch(url, {
headers: { 'X-API-Key': process.env.ZKPAY_API_KEY! },
signal: AbortSignal.timeout(8_000),
});
const body = (await response.json()) as Envelope<{ orders: MerchantOrderListItem[]; total: number }>;
if (!body.success || body.responseObject === null) {
throw new Error(`${body.statusCode}: ${body.message}`);
}
return body.responseObject.orders[0] ?? null;
}
A page of every open order a customer is actively paying:
curl "https://api.pay.peer.xyz/api/v1/merchants/me/orders?displayStatus=ACTIVE&limit=100" \
-H "X-API-Key: $ZKPAY_API_KEY"
Errors
| Status | Cause |
|---|---|
400 | A query parameter failed validation. Detail in responseObject.fieldErrors |
401 | Missing or invalid API key |
429 | Rate limit exceeded |
An orderId that matches nothing is not an error. You get 200 with an empty orders
array and total: 0.
Get an order by id
GET /api/v1/orders/{orderId}
Returns the same view the hosted checkout page renders: the order, the merchant branding it shows, and the customer's most recent payment attempt. No API key is needed, and one sent along is ignored; the order id is the credential. Order ids are random and unguessable, so treat them like any other capability URL and do not publish them.
This is the read to use when all you hold is an order id, for example from a
checkout.closed event or a success
redirect, and you want to know whether the customer actually paid.
Response
type HostedOrder = {
order: CheckoutOrder;
merchant: CheckoutMerchant;
currentPayment: CheckoutPayment | null;
};
type CheckoutMerchant = {
id: string;
name: string;
logoUrl: string | null;
environment: 'LIVE' | 'SANDBOX';
disableBranding: boolean;
checkoutTheme: unknown | null;
defaultPaymentCurrency: string | null;
verified: boolean;
};
| Field | Description |
|---|---|
order | The CheckoutOrder, including the hosted-only requestedFiat (the fiat price you asked for, present only on an un-resized fiat-input order) and bridgeInfo (cross-chain payout state for the current payment) |
merchant | What checkout shows about you. environment tells you whether this is a sandbox order |
currentPayment | The newest CheckoutPayment, or null when the customer has never picked a payment method. Read its status, rail, paymentAmount, and currency for where the customer is |
When the most recent settlement attempt on currentPayment failed on-chain, the payment
carries errorCode: "FULFILL_INTENT_FAILED" and a human-readable errorMessage.
currentPayment is one attempt, the latest. A customer who let one attempt expire and
started another shows only the second. Every attempt is visible through the
Payments API.
Example
curl https://api.pay.peer.xyz/api/v1/orders/cmf5k2x9d0001abcd1234efgh
{
"success": true,
"message": "Order retrieved",
"responseObject": {
"order": {
"id": "cmf5k2x9d0001abcd1234efgh",
"status": "CREATED",
"requestedUsdcAmount": "25",
"remainingUsdcAmount": "25",
"requestedFiat": null,
"notes": { "merchantOrderId": "order_12345" },
"createdAt": "2026-09-03T10:15:42.318Z",
"...": "..."
},
"merchant": {
"id": "cmf1abc...",
"name": "Example Store",
"environment": "LIVE",
"...": "..."
},
"currentPayment": {
"id": "cmf5k3...",
"status": "CREATED",
"rail": "venmo",
"paymentAmount": "25.38",
"currency": "USD",
"quoteExpiresAt": "2026-09-03T11:15:42.318Z",
"...": "..."
}
},
"statusCode": 200
}
Errors
| Status | Cause |
|---|---|
404 | No order with that id, in any environment |
429 | Rate limit exceeded |
What you cannot do with an API key
- Cancel an order.
POST /api/v1/merchants/me/orders/{orderId}/cancelneeds a dashboard session. Cancel from the dashboard's order list. An open order that is never cancelled staysCREATEDindefinitely; it costs nothing and reserves no liquidity until a customer starts paying. - Read
GET /api/v1/merchants/me/orders/{orderId}. That merchant-scoped single-order route also needs a dashboard session. Use theorderIdfilter on list orders instead; it returns the same order data.