Payments
A payment is one customer attempt to pay an order: they picked a rail, got a quote, and had a window to send the money (one hour on fiat rails, about six hours on crypto rails). An order can have several, because an attempt can expire or be cancelled and the customer can start again. This endpoint lists them across all your orders, newest first.
Payment status
| Status | Meaning |
|---|---|
CREATED | In progress. The customer is inside their payment window (quoteExpiresAt) and the quote in quote is reserved for them |
SETTLED | The fiat payment was verified and USDC was delivered on-chain. fulfillTransaction holds the transaction hash and netSettledUsdcAmount what you received after fees |
EXPIRED | The window closed without a verified payment. PAYMENT_EXPIRED fired. The order stays open |
CANCELLED | The customer switched payment method or backed out before paying |
FAILED | The attempt could not be started on-chain, or a live crypto transfer failed or never arrived. A rejected verification leaves the payment CREATED so the customer can retry. errorMessage says why when known. errorCode is null on list rows; only the hosted order read sets it |
SETTLED is the state your accounting cares about. A settled payment can later carry a
chargeback in chargeback and chargebackStatus; that is additive and does not change
status. See chargeback status.
List payments
GET /api/v1/merchants/me/payments
X-API-Key: <your key>
Query parameters
| Parameter | Type | Description |
|---|---|---|
status | CheckoutPaymentStatus | CREATED, SETTLED, EXPIRED, CANCELLED, or FAILED |
chargebackStatus | PaymentChargebackStatus | NONE for payments with no chargeback, CHARGEBACKED for payments with one |
page | number | 1-based page, default 1 |
limit | number | Rows per page, default 20, max 100 |
There is no order filter on this endpoint. For every attempt on one order, use
list payments for an order. For the attempt a customer is on right
now, currentPayment on the hosted read is enough. A
filtered pull by status=SETTLED is small enough to walk for reconciliation.
Response
responseObject is a page of CheckoutPayment rows:
type MerchantPaymentList = {
payments: CheckoutPayment[];
total: number;
page: number;
limit: number;
};
The fields that matter for reconciliation:
| Field | Description |
|---|---|
orderId | The order this attempt belongs to |
rail | Payment platform the customer used, for example venmo or zelle. See payment platforms |
paymentAmount, currency | What the customer was asked to send, in their fiat currency |
currencyPerUsdRate | The FX rate snapshotted for that currency at quote time |
penalties | Always-present PaymentPenalty[] of attestation-applied reductions (Venmo/PayPal purchase protection, cross-currency), in application order. Empty when none apply; a non-empty array explains a netSettledUsdcAmount lower than expected |
netSettledUsdcAmount | USDC you received after all fees. null until SETTLED |
totalUsdcFeeAmount | Total fees taken from the settlement in USDC |
referralFees | Per-recipient referral fee breakdown, when any applied |
railIdentifier | Opaque reference for the attempt: the on-chain identifier on fiat rails, the transfer request id on crypto rails. It is assigned when the attempt is registered, before any funds move, and is null until then. It changes only when a recreate reopens the attempt; id never changes, so key external references on id |
fulfillTransaction | Settlement transaction hash. null until SETTLED |
quoteExpiresAt | When the customer's window closes |
completedAt | When the payment reached a terminal state |
Example
Every settled payment, most recent first:
curl "https://api.pay.peer.xyz/api/v1/merchants/me/payments?status=SETTLED&limit=100" \
-H "X-API-Key: $ZKPAY_API_KEY"
Walking all pages to total what settled:
type Envelope<T> = { success: boolean; message: string; responseObject: T | null; statusCode: number };
type PaymentPage = { payments: CheckoutPayment[]; total: number; page: number; limit: number };
// "12.5" -> 12500000n. Scale to integers instead of parseFloat.
const toMicroUsdc = (amount: string): bigint => {
const [whole, fraction = ''] = amount.split('.');
return BigInt(whole) * 1_000_000n + BigInt(fraction.padEnd(6, '0').slice(0, 6));
};
async function sumSettledMicroUsdc(): Promise<bigint> {
let page = 1;
let settled = 0n;
for (;;) {
const url = new URL('https://api.pay.peer.xyz/api/v1/merchants/me/payments');
url.searchParams.set('status', 'SETTLED');
url.searchParams.set('page', String(page));
url.searchParams.set('limit', '100');
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<PaymentPage>;
if (!body.success || body.responseObject === null) {
throw new Error(`${body.statusCode}: ${body.message}`);
}
for (const payment of body.responseObject.payments) {
settled += toMicroUsdc(payment.netSettledUsdcAmount ?? '0');
}
if (page * body.responseObject.limit >= body.responseObject.total) break;
page += 1;
}
return settled;
}
A chargeback pull for the finance team:
curl "https://api.pay.peer.xyz/api/v1/merchants/me/payments?chargebackStatus=CHARGEBACKED" \
-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 |
List payments for an order
GET /api/v1/merchants/me/orders/{orderId}/payments
X-API-Key: <your key>
Every attempt on one of your orders, newest first, as full
CheckoutPayment rows. There is no pagination; an order rarely
has more than a handful of attempts. When a customer says they paid, railIdentifier
identifies the attempt (on-chain for fiat rails, by transfer request id on crypto rails;
null until the attempt is registered) and quoteExpiresAt is its window. payTo is an opaque recipient identifier, not a readable handle.
Remediation acts on these rows.
Response
type OrderPayments = { payments: CheckoutPayment[] };
Example
curl "https://api.pay.peer.xyz/api/v1/merchants/me/orders/cmf5k2x9d0001abcd1234efgh/payments" \
-H "X-API-Key: $ZKPAY_API_KEY" \
| jq '.responseObject.payments[] | {id, status, rail, payTo, railIdentifier, quoteExpiresAt}'
Errors
| Status | Cause |
|---|---|
401 | Missing or invalid API key |
404 | The order does not exist, belongs to another merchant, or lives in the other environment (sandbox vs live) |
429 | Rate limit exceeded. This read also counts toward the per-merchant remediation read limit |
Notes
- Payment rows are the same objects that arrive as
data.paymentin webhook payloads. Reconcile with the same code. - A sandbox key lists sandbox payments only. Sandbox settlements are simulated: no money
moves and nothing settles on-chain, so do not look up a sandbox
fulfillTransactionon a block explorer. - Amounts are decimal strings. Parse them with a decimal library or scale to integers, as
the example does, rather than with
parseFloat.