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.
Amount mode
Every order carries amountMode. Read it before the amounts:
amountMode | Created with | Amounts |
|---|---|---|
FIXED | requestedUsdcAmount, or requestedFiatAmount with requestedFiatCurrency | requestedUsdcAmount and remainingUsdcAmount are always set |
OPEN | openAmount | requestedUsdcAmount and remainingUsdcAmount are null until the customer starts a payment. The order also carries openAmount (your currency, minAmount, maxAmount and presets) and amountVersion, which counts how many times the amount was saved |
See open-amount orders.
Order status
status is CREATED, PARTIALLY_FULFILLED, FULFILLED, or CANCELLED. It tracks how much
of the order's value has settled, not where the customer is in the payment flow. What each
value means, what moves it, and which webhook fires are in
order status.
An order never expires on its own. The customer payment window expires the payment
attempt 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: they sit in chargebackStatus, chargebacks, and the
refund* fields and never move status. 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 and limit follow the shared pagination rules.
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 JSON you
attached at creation, the API's internal metadata JSON, the payment rail (by identifier or
by display name, so venmo and Venmo both match), and the payment's id, payTo
(recipient handle/address), railIdentifier, railTransactionId (payment-app transaction ID),
and fulfillTransaction (settlement transaction hash).
Plain non-negative decimals, optionally prefixed with $, also match the requested fiat
amount in order metadata or requestedUsdcAmount by exact numeric equality. For example,
$20 and 20.00 match 20.00. Amount searches follow the same three-character minimum,
so use $20 or 20.00, not 20.
Existing substring matches still apply; % and _ are treated literally.
metadata is written by the API, not by you; notes is the merchant field. 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;
netSettledUsdcAmount: string | null;
settledPaymentRails: string[];
settledPaymentCount: number;
};
The six extras are dashboard list signals, derived from payment state rather than stored on the order:
| 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 |
netSettledUsdcAmount | USDC credited to you across settled payments, after fees. Chargebacks and refunds are not subtracted. null when nothing has settled or any settled payment has no recorded amount. |
settledPaymentCount | Number of settled payments, including payments on the same rail. Zero when nothing has settled; refunds require a single settled payment. |
settledPaymentRails | Distinct rails of the settled payments, oldest payment first (by when the payment was created). Empty when nothing has settled. |
cryptoReferralSplitConfig, the referral split that applies when an order is paid over a
crypto rail, is not a list-only extra: it is on the order object every read and every webhook
returns. See CheckoutOrder.
List rows also carry bridgeInfo: conversion info from the newest settled payment
that has a conversion attempt, using its latest attempt (both by creation time).
It is null when no settled payment has a conversion attempt. The
hosted read uses the current payment instead.
requestedFiat remains hosted-only.
Example
curl "https://api.pay.peer.xyz/api/v1/merchants/me/orders?orderId=cmf5k2x9d0001abcd1234efgh" \
-H "X-API-Key: $ZKPAY_API_KEY"
Envelope<T> below is the shared response envelope.
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
The common errors are the only ones this endpoint returns. An orderId
that matches nothing is not one of them: 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 current 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 requestedFiat (the fiat price you asked for, present only on an un-resized fiat-input order; on an open-amount order, the saved amount in the currency the customer typed it in), bridgeInfo (cross-chain payout state for the current payment) and applePayChargeUsdcAmount (the USDC the customer would be charged for the remaining balance by Apple Pay, grossed up for Apple Pay fees when the customer pays them; null when Apple Pay is not enabled for the order, the charge cannot be priced, or an open-amount order has no saved amount yet). An open-amount order also carries openAmount with effectiveMin, effectiveMax (the allowed range in openAmount.currency, never above 10,000 USDC worth; both null while the amount is locked, once the order is fulfilled or cancelled, or while the exchange rate is unavailable) and savedInput (in the currency the customer typed), and amountLock |
merchant | What checkout shows about you. environment tells you whether this is a sandbox order |
currentPayment | The newest CheckoutPayment, except that if it was cancelled, expired or failed, the newest older fiat attempt with an open, unexpired on-chain intent is returned; if none is open and the newest attempt was a fiat attempt, the newest settled attempt is returned when one exists. A cancelled, expired or failed crypto attempt otherwise stays current. A funded Zcash deposit still takes priority while converting. Returns null when the customer has never picked a payment method. Read its status and rail for where the customer is, and quote with quoteExpiresAt for what they owe and until when. paymentAmount is "0" until the attempt settles |
When the most recent settlement attempt on currentPayment failed onchain, the payment
carries errorCode: "FULFILL_INTENT_FAILED" and a human-readable errorMessage.
currentPayment is one attempt selected by this rule. A new active or settled
attempt stays current; cancelling, expiring or failing a newer attempt can reveal
an older fiat intent that the customer can still pay or finish, or, for a fiat
attempt, the newest settled attempt when no older fiat intent is open. 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",
"amountMode": "FIXED",
"requestedUsdcAmount": "25",
"remainingUsdcAmount": "25",
"applePayChargeUsdcAmount": "26.31579",
"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": "0",
"currency": "USD",
"quoteExpiresAt": "2026-09-03T11:15:42.318Z",
"...": "..."
}
},
"statusCode": 200
}
For an open-amount order that no customer has started paying,
currentPayment is null and order reads like this:
{
"id": "cmf5k2x9d0001abcd1234efgh",
"status": "CREATED",
"amountMode": "OPEN",
"requestedUsdcAmount": null,
"remainingUsdcAmount": null,
"amountVersion": 0,
"openAmount": {
"currency": "USD",
"minAmount": "10.00",
"maxAmount": "2000.00",
"presets": ["25.00", "50.00", "100.00"],
"effectiveMin": "10.00",
"effectiveMax": "2000.00",
"savedInput": null
},
"amountLock": null,
"applePayChargeUsdcAmount": null,
"requestedFiat": null,
"...": "..."
}
Errors
| Status | Cause |
|---|---|
404 | No order with that id, in any environment |
The common errors also apply, except 401: this read takes no API key.
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.