Skip to main content

createCheckout

Creates an order and returns an authenticated checkout URL.

Signature​

function createCheckout(
params: CreateOrderRequest,
opts: CheckoutClientOptions
): Promise<CreateCheckoutResult>

Parameters​

params: CreateOrderRequest​

PropertyTypeRequiredDescription
requestedUsdcAmountstringConditionalUse this for direct USDC mode. Mutually exclusive with fiat-mode fields.
requestedFiatAmountstringConditionalFiat-mode input amount. Requires requestedFiatCurrency.
requestedFiatCurrencystringConditionalFiat-mode ISO currency code (for example EUR). Requires requestedFiatAmount.
openAmountOpenAmountInputConditionalOpen-amount mode: the customer types the amount in checkout. { currency, minAmount?, maxAmount?, presets? }. See Open-Amount Orders.
idempotencyKeystringNoStable purchase key for retry deduplication, 8–128 letters, digits, underscores or hyphens. See below.
destinationAddressstringNoOverride destination wallet (falls back to your merchant payout wallet for the destination chain when omitted)
destinationTokenstringNoOverride destination token alias/address
destinationChainIdnumberNoOverride destination chain
feePayerFeePayerTypeNoMERCHANT, PAYEE, or SPLIT fee mode override
buyerFeeShareBpsnumberNoRequired with explicit SPLIT: buyer share of total fees in basis points (5000 = 50%, 0–10000 in steps of 1000)
dynamicOrdersEnabledbooleanNoPer-order Dynamic Orders opt-out. false disables the feature for this order; omitted inherits the merchant default. Passing true when the merchant setting is disabled rejects order creation with 400 DYNAMIC_ORDERS_DISABLED. The value is snapshotted at order creation. Open-amount orders cannot pass true.
enabledRailsstring[]NoRestrict available rails. Non-empty; each value must be a supported rail identifier
successUrlstring | nullNoAbsolute URL for the Return to merchant link shown after a successful payment (defaults to null). See what the customer actually sees
cancelUrlstring | nullNoAbsolute URL stored on the order and echoed back to you (defaults to null). Checkout never navigates to it. See what the customer actually sees
notesRecord<string, unknown> | nullNoMerchant notes payload (defaults to null)

Provide exactly one amount mode:

  • USDC mode: requestedUsdcAmount
  • Fiat mode: requestedFiatAmount + requestedFiatCurrency
  • Open-amount mode: openAmount

A request with none of them fails with 400. Leaving the amount out never creates an open-amount order.

In fiat mode the amount is converted to USDC at the current rate and rounded to 2 decimals. The order's USDC amount fields carry the result. In open-amount mode the order has no amount until the customer starts a payment: requestedUsdcAmount and remainingUsdcAmount are null and amountMode is "OPEN". See Open-Amount Orders.

Order amount limits​

Orders are limited to 10,000 USDC, the most the protocol can settle in a single request. Every order must also be at least the platform minimum (10 USDC by default). Both limits apply to sandbox merchants too.

  • A fixed order above 10,000 USDC fails with 400 AMOUNT_ABOVE_MAX. Exactly 10,000 USDC is allowed. In fiat mode the converted USDC amount is checked, so 9,500 EUR at 0.92 EUR per USD (10,326.09 USDC) is refused.
  • Dynamic Orders never suggest a nearby amount above 10,000 USDC, or one whose payment would settle above the per-payment limit below. A resize above 10,000 USDC fails the same way.
  • An open-amount order's allowed range never goes above 10,000 USDC. See Open-Amount Orders.

The 10,000 USDC limit applies to each payment too, and it includes fees. When the customer pays by bank or app (Venmo, Cash App, Zelle and the other fiat methods), the amount the payment settles on chain must be at most 10,000 USDC.

  • With feePayer: "MERCHANT" and a Base USDC payout, that settled amount is normally the order amount or a little less, so a 10,000 USDC order fits. If the best quote would still settle above the limit, the payment start fails and is not retried with the next seller's quote.
  • When the customer pays the fees (PAYEE, or the buyer's share under SPLIT), the fees are added first. A 10,000 USDC PAYEE order with 1.5% fees settles about 10,152 USDC, so bank and app methods cannot take it. At 1.5% the largest PAYEE order they can take is about 9,850 USDC.
  • A payout outside Base USDC adds the bridge cost the same way, whoever pays the fees.

Over the limit, checkout lists the affected bank and app methods as unavailable for that amount. A payment started on one anyway fails with 400 INTENT_ABOVE_MAX. Crypto payments are not affected, because they do not settle through the per-payment escrow. Apple Pay has its own lower limit.

For a larger purchase, create several orders.

Idempotent order creation​

The request body of POST /api/v1/orders has the same shape. Pass a stable idempotencyKey per purchase and reuse it on retries, including timeouts. A replay returns the original order; a different amount or mode returns 409 IDEMPOTENCY_KEY_CONFLICT. Other fields do not update the original order.

A replay returns idempotentReplay: true, orderToken: null and checkoutUrl: null. Save the first checkout URL on your backend and check for a null URL before redirecting. The API does not recover a lost token on replay. createCheckoutAndRedirect returns the replay without navigating.

See integration facts at a glance.

opts: CheckoutClientOptions​

apiBaseUrl and apiKey are required, and you should always set checkoutBaseUrl so the returned checkoutUrl points at the checkout host rather than the API host. Every field is documented in Configuration.

Returns​

type CreateCheckoutResult = {
order: CheckoutOrder;
orderToken: string | null;
checkoutUrl: string | null;
idempotentReplay?: true;
};

For a newly created order, checkoutUrl includes the order token. A replay has no checkout URL.

Example​

import { createCheckout } from '@zkp2p/pay-sdk';

const checkout = await createCheckout(
{
requestedFiatAmount: '75.00',
requestedFiatCurrency: 'EUR',
destinationChainId: 8453,
destinationToken: 'USDC',
destinationAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE71',
successUrl: 'https://example.com/success',
cancelUrl: 'https://example.com/cancel',
notes: {
orderId: 'order_12345',
customerId: 'cust_67890',
},
},
{
apiBaseUrl: 'https://api.pay.peer.xyz',
checkoutBaseUrl: 'https://pay.peer.xyz',
apiKey: process.env.ZKPAY_API_KEY!,
}
);

console.log(checkout.order.id); // cmf5k2x9d0001abcd1234efgh
console.log(checkout.checkoutUrl);

What successUrl and cancelUrl do​

Neither URL moves the customer on its own.

  • successUrl: after a successful payment, checkout shows a Return to merchant link the customer clicks. It is not an automatic redirect, and it is not shown in embedded mode. The link carries order_id, payment_id or request_id, intent_hash, tx_hash, and status=success as query parameters.
  • cancelUrl: stored on the order and echoed back to you. Checkout never navigates to it.

Treat both as convenience navigation only. Never fulfill an order on a redirect or on a query parameter. Fulfill on the ORDER_FULFILLED webhook.

Errors​

A rejected request throws PayApiError. Branch on errorCode; the message is written for a log line, not for matching.

StatuserrorCodeCause
400(none)A field failed validation. Detail on fieldErrors and formErrors
400AMOUNT_BELOW_MINThe amount is below the platform minimum (10 USDC by default). Applies to sandbox too
400AMOUNT_ABOVE_MAXThe amount is above 10,000 USDC, the protocol's per-request limit. Fiat mode checks the converted USDC amount. Applies to sandbox too. See order amount limits
400AMOUNT_NON_POSITIVEFiat mode converted to zero or less
400AMOUNT_BELOW_MERCHANT_MIN, AMOUNT_ABOVE_MERCHANT_MAXThe amount is outside the order-size range configured for your account. Contact Peer to change it. Not enforced for sandbox merchants
400DESTINATION_INVALIDNo payout address is configured for the destination chain, or the supplied destinationAddress does not match that chain's address format
400PAYOUT_CONFIG_INVALIDUnsupported destination chain or token, or a non-Base payout on an account that has not enabled receiving outside Base USDC
400NO_ELIGIBLE_PAYMENT_RAILSNone of the rails the order allows can be offered. Each is disabled, or excluded by your account's verification settings
400MERCHANT_CONFIG_MISSINGThe account has no checkout configuration yet. Finish onboarding in the dashboard
400DYNAMIC_ORDERS_DISABLEDdynamicOrdersEnabled: true on a merchant with the setting off
400OPEN_AMOUNT_INVALIDopenAmount is invalid, its currency has no exchange rate, its maxAmount converts to more than 10,000 USDC, its allowed range is empty, a preset is outside it, or it is combined with another amount mode, dynamicOrdersEnabled: true or inPersonCheckout: true. The message names the field
400OPEN_AMOUNT_DISABLEDOpen-amount orders are switched off for this environment
401(none)Missing or invalid API key
403MERCHANT_TIER_FORBIDDENA live key with no plan selected. Sandbox keys never hit this. Select Base or Pro under Settings, Billing
403MERCHANT_MONTHLY_ORDER_LIMIT_EXCEEDEDThe plan's monthly fiat order count is reached (Base 50, Pro 100, per UTC month)
403MERCHANT_MONTHLY_VOLUME_LIMIT_EXCEEDEDThe order would take fiat-paid volume past the plan's monthly cap (Base $10K, Pro $30K). An open-amount order has no amount yet, so its volume is checked when the customer starts a payment, not at creation
409IDEMPOTENCY_KEY_CONFLICTThe idempotencyKey was already used with a different amount or mode, including a fixed request against an open-amount order and the reverse
409MERCHANT_OWNERSHIP_CHANGEDThe merchant account changed owners while the order was being created. Nothing was saved; retry with the same idempotencyKey to create it for the new owner
502EXCHANGE_RATE_LOOKUP_FAILEDThe rate lookup failed for fiat mode with a non-USD currency, or for an open-amount order whose openAmount.currency is not USD. Retry

Starting a payment can fail with 400 INTENT_ABOVE_MAX, which createCheckout never returns. It means a bank or app payment on the order would settle more than 10,000 USDC once fees are added, for example This payment would be 10152.29 USDC with fees, above the 10000.00 USDC limit per payment. It applies to sandbox too. See order amount limits.

Monthly caps count fiat-paid orders created in the current UTC month, at their full requested amount: an order counts once any fiat payment on it settles, so an abandoned checkout does not consume them and orders paid only with crypto never do. The checks run at creation, before the customer picks how to pay. For an open-amount order, the volume check runs when the customer starts a payment with a new amount instead. Concierge and sandbox merchants are never capped. See plans and support.