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.
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
| Property | Type | Required | Description |
|---|---|---|---|
amount | string | Yes | Positive decimal string. Interpreted by quoteMode |
quoteMode | CheckoutModeType | Yes | 'exact-token' or 'exact-fiat' — see below |
destinationChainId | string | number | Yes | Payout chain, decimal only — 8453 or '8453' for Base. Hex ('0x2105'), '+8453', and leading zeros are rejected |
destinationToken | string | Yes | Payout token, for example 'USDC' |
destinationAddress | string | Yes | Payout address |
fiatCurrency | string | No | Falls back to your configured default payment currency, then to USD |
nearbyQuotesCount | number | No | Suggestions 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_TOKEN—amountis the USDC you want to receive. The customer pays a variable fiat amount.CheckoutMode.EXACT_FIAT—amountis the fiat the customer pays, denominated infiatCurrency. 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;
};
| Property | Type | Description |
|---|---|---|
available | boolean | Whether at least one quote can currently fill amount |
quoteCount | number | How many quotes matched |
nearbySuggestions | object | null | Alternative amounts. Always null when available is true |
Each suggestion:
| Property | Type | Description |
|---|---|---|
suggestedAmount | string | The alternative amount, in your quoteMode units |
percentDifference | string | Signed difference from your requested amount |
rail | string | Payment rail that can fill it, for example 'venmo' |
paymentAmount | string | What the customer pays |
paymentCurrency | string | Currency of paymentAmount |
tokenAmount | string | USDC you would receive |
conversionRate | string | Rate 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.
| Status | Message | Cause |
|---|---|---|
400 | Invalid request | A field failed validation. The per-field errors are in the response body, but the SDK discards them — see Retries to read them |
400 | Merchant config not found | The merchant has no configuration yet |
401 | — | Missing or invalid API key |
429 | Too many requests | Rate limited. See the x-retry-after header below |
502 | Unable to resolve exchange rate for <CUR> | Exchange-rate lookup failed for that currency |
502 | Quote provider unavailable | Upstream quote provider failed |
502 | Unable to price non-Base payout for quote availability | The non-Base payout could not be priced |
500 | — | Unexpected 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:
| Header | Value |
|---|---|
x-retry-after | Seconds until you may retry |
x-rate-limit-remaining | Requests left in the current window |
x-rate-limiter-resets-at | ISO 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:
| Message | Meaning |
|---|---|
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 response | The 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.