Skip to main content

Orders

An order is the thing you create with createCheckout. This page covers reading it back. There are two reads:

EndpointAuthUse it when
GET /api/v1/merchants/me/ordersAPI keyYou are on your backend and want one order by id, or a filtered page of orders
GET /api/v1/orders/{orderId}NoneYou 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.

StatusMeaning
CREATEDOpen. Nothing has settled yet. The customer may not have started, may be mid-payment, or may have let an attempt expire
PARTIALLY_FULFILLEDSome 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
FULFILLEDSettled. 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
CANCELLEDCancelled 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

ParameterTypeDescription
orderIdstringCase-insensitive substring match on the order id. A full id matches exactly one order. Ignored when search is set
statusCheckoutOrderStatusCREATED, PARTIALLY_FULFILLED, FULFILLED, or CANCELLED. Ignored when displayStatus is set
displayStatusstringSlices open orders by payment activity. See display status
chargebackStatusOrderChargebackStatusNONE, PARTIALLY_CHARGEBACKED, or CHARGEBACKED
searchstringFree-text search, at least 3 characters. Shorter values are ignored. See search
pagenumber1-based page, default 1
limitnumberRows 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.

ValueMatches an open order that
ACTIVEHas a payment attempt in progress, so a customer is inside their payment window
EXPIREDHas at least one expired attempt and none in progress. The customer walked away
CREATEDHas no attempt in progress and none expired. Either the customer never picked a payment method, or every attempt was cancelled or failed

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 fieldDescription
hasActivePaymentA payment attempt is in progress
hasExpiredPaymentAt least one attempt expired
paymentSelectedRailRail of the attempt in progress, else of the most recent expired one, else null
cryptoReferralSplitConfigReferral 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

StatusCause
400A query parameter failed validation. Detail in responseObject.fieldErrors
401Missing or invalid API key
429Rate 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;
};
FieldDescription
orderThe 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)
merchantWhat checkout shows about you. environment tells you whether this is a sandbox order
currentPaymentThe 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

StatusCause
404No order with that id, in any environment
429Rate limit exceeded

What you cannot do with an API key

  • Cancel an order. POST /api/v1/merchants/me/orders/{orderId}/cancel needs a dashboard session. Cancel from the dashboard's order list. An open order that is never cancelled stays CREATED indefinitely; 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 the orderId filter on list orders instead; it returns the same order data.