Skip to main content

Quote Availability

checkQuoteAvailability answers one question from your backend, before you create an order: can this amount be paid right now? When it cannot, the response carries nearby amounts that can.

Server-side only

This call sends your merchant API key in the X-API-Key header. Call it only from a trusted backend. Bundling it into browser code exposes your API key to every visitor.

Basic usage

import { checkQuoteAvailability, CheckoutMode } from '@zkp2p/pay-sdk';

const availability = await checkQuoteAvailability(
{
amount: '25.00',
quoteMode: CheckoutMode.EXACT_TOKEN,
destinationChainId: 8453,
destinationToken: 'USDC',
destinationAddress: '0xYourPayoutWallet',
},
{
apiBaseUrl: 'https://api.pay.peer.xyz',
apiKey: process.env.ZKPAY_API_KEY!,
signal: AbortSignal.timeout(8_000),
},
);

if (availability.available) {
// Create the order as normal.
}

Pass a signal so a slow upstream cannot block your own request handler.

Parameters

params: QuoteAvailabilityRequest

PropertyTypeRequiredDescription
amountstringYesPositive decimal string. Interpreted by quoteMode
quoteModeCheckoutModeTypeYes'exact-token' or 'exact-fiat' — see below
destinationChainIdstring | numberYesPayout chain, decimal only8453 or '8453' for Base. Hex ('0x2105'), '+8453', and leading zeros are rejected
destinationTokenstringYesPayout token, for example 'USDC'
destinationAddressstringYesPayout address
fiatCurrencystringNoFalls back to your configured default payment currency, then to USD
nearbyQuotesCountnumberNoSuggestions per direction, 1–10, default 3

CheckoutModeType is the type; CheckoutMode is the const object you read values from (CheckoutMode.EXACT_TOKEN). Annotate with CheckoutModeType.

exact-token vs exact-fiat

  • CheckoutMode.EXACT_TOKENamount is the USDC you want to receive. The customer pays a variable fiat amount.
  • CheckoutMode.EXACT_FIATamount is the fiat the customer pays, denominated in fiatCurrency. You receive a variable USDC amount.

Use the same mode you intend to use at order creation. Suggested amounts come back in the units of the mode you asked for.

Response

type QuoteAvailability = {
available: boolean;
quoteCount: number;
nearbySuggestions: {
below: QuoteAvailabilitySuggestion[];
above: QuoteAvailabilitySuggestion[];
} | null;
};
PropertyTypeDescription
availablebooleanWhether at least one quote can currently fill amount
quoteCountnumberHow many quotes matched
nearbySuggestionsobject | nullAlternative amounts. Always null when available is true

Each suggestion:

PropertyTypeDescription
suggestedAmountstringThe alternative amount, in your quoteMode units
percentDifferencestringSigned difference from your requested amount
railstringPayment rail that can fill it, for example 'venmo'
paymentAmountstringWhat the customer pays
paymentCurrencystringCurrency of paymentAmount
tokenAmountstringUSDC you would receive
conversionRatestringRate used for the quote

below holds amounts smaller than what you asked for, above larger.

Four behaviours worth knowing

Suggestions only appear when the amount is unavailable

nearbySuggestions is always null when available is true. It is an unavailability fallback, not a general listing of other amounts that would also work. There is no way to ask "is this fillable, and what else is fillable too?" in a single call.

available: false does not guarantee suggestions

You can get either nearbySuggestions: null or nearbySuggestions: { below: [], above: [] }. Both mean "no alternatives to offer" — which one you get depends on where in the pipeline the candidates ran out, and that is an internal detail you should not branch on.

Handle both. Checking nearbySuggestions !== null is not enough; check array length too.

nearbyQuotesCount is per direction

It caps below and above independently. nearbyQuotesCount: 10 can return up to 20 suggestions in total.

available: true is advisory

The API probes live liquidity and reserves nothing. Liquidity can move between your check and your order creation, so order creation can still fail. Treat this as a signal for what to show the customer, not a guarantee — always handle order-creation failure.

Where the destination fields come from

All three destination fields are required here, even though createCheckout treats them as optional and falls back to your merchant configuration.

getMerchant supplies two of them:

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

const merchant = await getMerchant({ apiBaseUrl, apiKey });

if (merchant.merchantConfig === null) {
throw new Error('Merchant config is not set up yet.');
}

const { destinationChainId, destinationToken } = merchant.merchantConfig;

There is no destinationAddress on merchantConfig. Pass the same destinationAddress you pass to createCheckout. If you omit it there and rely on configuration defaults, use the payout address from your dashboard payout settings.

Re-read this per checkout flow rather than caching it indefinitely — payout configuration can change.

Worked example

Check first, then either create the order or offer the customer an amount that works:

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

const params = {
amount: '25.00',
quoteMode: CheckoutMode.EXACT_TOKEN,
destinationChainId: 8453,
destinationToken: 'USDC',
destinationAddress: '0xYourPayoutWallet',
};

// A fresh signal per call. An AbortSignal.timeout starts counting the moment it is
// created, so sharing one object across two sequential calls gives the second only
// whatever is left — and aborts it outright if the first used the full budget.
const opts = () => ({
apiBaseUrl: 'https://api.pay.peer.xyz',
checkoutBaseUrl: 'https://pay.peer.xyz',
apiKey: process.env.ZKPAY_API_KEY!,
signal: AbortSignal.timeout(8_000),
});

const availability = await checkQuoteAvailability(params, opts());

if (availability.available) {
const checkout = await createCheckout(
{ requestedUsdcAmount: params.amount, ...orderFields },
opts(),
);
return { checkoutUrl: checkout.checkoutUrl };
}

const suggestions = availability.nearbySuggestions;
const alternatives = [
...(suggestions?.below ?? []),
...(suggestions?.above ?? []),
];

if (alternatives.length === 0) {
return { status: 'no_liquidity' };
}

return {
status: 'alternatives',
options: alternatives.map((option) => ({
amount: option.suggestedAmount,
rail: option.rail,
customerPays: `${option.paymentAmount} ${option.paymentCurrency}`,
})),
};

Common errors

The SDK throws an Error carrying the server's message. This table covers what you are most likely to hit; it is not exhaustive.

StatusMessageCause
400Invalid requestA field failed validation. The per-field errors are in the response body, but the SDK discards them — see Retries to read them
400Merchant config not foundThe merchant has no configuration yet
401Missing or invalid API key
429Too many requestsRate limited. See the x-retry-after header below
502Unable to resolve exchange rate for <CUR>Exchange-rate lookup failed for that currency
502Quote provider unavailableUpstream quote provider failed
502Unable to price non-Base payout for quote availabilityThe non-Base payout could not be priced
500Unexpected server error

Standard transport-level failures — a malformed request body, an oversized body, a wrong route — return the same response envelope.

Retries

The thrown Error carries the server's message but not the HTTP status, the response headers, or the validation details from a 400. To reach any of those, pass a custom fetcher and inspect the response before returning it:

const fetcher: typeof fetch = async (input, init) => {
const response = await fetch(input, init);

if (response.status === 429) {
// Seconds until the limit resets. Note the x- prefix — this API does not
// send a standard `Retry-After` header.
recordRetryAfter(Number(response.headers.get('x-retry-after')));
}

return response;
};

Rate-limited responses carry three headers:

HeaderValue
x-retry-afterSeconds until you may retry
x-rate-limit-remainingRequests left in the current window
x-rate-limiter-resets-atISO timestamp when the window resets

To read a 400's per-field validation errors, clone the response in your fetcher and parse responseObject before returning it — the SDK throws away everything except message.

Malformed responses

The SDK never hands back a partially-valid object. If a response does not match the documented shape it throws, and the message tells you which layer rejected it:

MessageMeaning
Malformed API response envelope: …The outer { success, responseObject } envelope was wrong or absent — usually a proxy or gateway returning something that is not the API's response
Malformed quote availability responseThe envelope was fine, but the availability payload was not — a bad quoteCount, or a suggestion missing one of its seven fields

Both are plain Errors. If you match on these strings, match on both.