Skip to main content

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

StatusMeaning
CREATEDIn progress. The customer is inside their payment window (quoteExpiresAt) and the quote in quote is reserved for them
SETTLEDThe fiat payment was verified and USDC was delivered on-chain. fulfillTransaction holds the transaction hash and netSettledUsdcAmount what you received after fees
EXPIREDThe window closed without a verified payment. PAYMENT_EXPIRED fired. The order stays open
CANCELLEDThe customer switched payment method or backed out before paying
FAILEDThe 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

ParameterTypeDescription
statusCheckoutPaymentStatusCREATED, SETTLED, EXPIRED, CANCELLED, or FAILED
chargebackStatusPaymentChargebackStatusNONE for payments with no chargeback, CHARGEBACKED for payments with one
pagenumber1-based page, default 1
limitnumberRows 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:

FieldDescription
orderIdThe order this attempt belongs to
railPayment platform the customer used, for example venmo or zelle. See payment platforms
paymentAmount, currencyWhat the customer was asked to send, in their fiat currency
currencyPerUsdRateThe FX rate snapshotted for that currency at quote time
penaltiesAlways-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
netSettledUsdcAmountUSDC you received after all fees. null until SETTLED
totalUsdcFeeAmountTotal fees taken from the settlement in USDC
referralFeesPer-recipient referral fee breakdown, when any applied
railIdentifierOpaque 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
fulfillTransactionSettlement transaction hash. null until SETTLED
quoteExpiresAtWhen the customer's window closes
completedAtWhen 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

StatusCause
400A query parameter failed validation. Detail in responseObject.fieldErrors
401Missing or invalid API key
429Rate 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

StatusCause
401Missing or invalid API key
404The order does not exist, belongs to another merchant, or lives in the other environment (sandbox vs live)
429Rate limit exceeded. This read also counts toward the per-merchant remediation read limit

Notes

  • Payment rows are the same objects that arrive as data.payment in 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 fulfillTransaction on 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.