Quote the next charge for a subscription
POST/subscriptions/{subscriptionId}/charge-quoteQuote the next charge for a subscription: the token amount to sign, plus the locked rate and the price_lock_code to submit with the charge. An optional amount in fiat minor units quotes an ad-hoc charge; omitted, the stored plan price is quoted. Every quote is checked against the subscription's cap_amount and the remaining per-window budget at quote time. Legacy token-denominated plans return the fixed amount with no lock.
Headers
| Header | Description | Required |
|---|---|---|
| Authorization | Bearer token with your secret API key | yes |
Path Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| subscriptionId | string | yes |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| amount | string | — | Optional ad-hoc charge amount in minor units of the subscription's price_currency. Omitted, the stored plan price is quoted. Bounded by the subscription's cap_amount and the remaining per-window budget. |
Example Request
Request
const response = await fetch('https://checkout-api.exodus.com/subscriptions/<subscriptionId>/charge-quote', {
method: 'POST',
headers: {
Authorization: 'Bearer sk_live_xxxxxxxxxxxxxxxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"amount": "3490000"
}),
});Responses
| Status | Description |
|---|---|
| 200 | The charge quote, including any locked rate. |
| 400 | Validation error |
| 401 | Missing or invalid authentication |
| 403 | API key lacks the required scope |
| 404 | Resource not found |
Response body
| Field | Type | Required | Description |
|---|---|---|---|
| object | enum: subscription_charge_quote | yes | |
| subscription_id | string | yes | The on-chain subscription id the quote is for. |
| amount | string | yes | The amount to sign and submit to the charge, in the settlement token's smallest units. It is the fiat amount converted at rate and rounded to a whole smallest unit. That fiat amount is the ad-hoc amount from the request body when given, else the plan price. |
| price_lock_code | string | null | yes | Code that binds this quote to the subscription, its token and chain, and amount. Pass it to the charge before expires_at. One successful charge consumes the code; a cleanly rejected charge releases it, so you can retry with it until expires_at. A charge whose on-chain outcome is undeterminable holds the code instead; re-quote rather than retry. Minted by the settlement provider on a fiat-settled business, or as a refq_-prefixed reference quote on a crypto-settled one. Legacy token-denominated plans, created before fiat-only creation shipped, return null here and in rate and expires_at; their amount is the plan price unchanged. |
| rate | string | null | yes | Conversion rate the quote applied, in whole units of price_currency per one whole token ("696.27" is 696.27 ARS per USDC). |
| expires_at | string | null | yes | ISO 8601 timestamp after which the quote can no longer be charged. Quote again past it. |
Example Response
{
"object": "subscription_charge_quote",
"subscription_id": "0x9f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a",
"amount": "9990000",
"price_lock_code": "refq_3f9a1c7d2e5b8064",
"rate": "696.27",
"expires_at": "2026-09-16T12:04:18Z"
}Error Response
Error (e.g. 400)
{
"error": {
"type": "validation_error",
"message": "amount must be a positive number",
"param": "amount"
}
}Quote Semantics
No signature — this call moves no funds. The body is optional: fiat-priced plans may pass amount, in minor units of the plan’s price_currency, to quote an ad-hoc charge instead of the stored price. Omit it to quote the plan price. Token-priced plans take no body.
- Token-priced plans (
price_currency: null) echo the plan’spriceasamount, withprice_lock_code,rate, andexpires_atallnull. You can skip the quote entirely and signpricedirectly. - Fiat-priced plans on a fiat-settled business convert the fiat
priceto a settlement-tokenamountat the live rate via the settlement provider and lock that rate: pass the returnedamountandprice_lock_codetoPOST /subscriptions/:id/chargebeforeexpires_at. The provider commits to converting at that rate through ramp-off. - Fiat-priced plans on a crypto-settled business (currently ARS-priced plans only) convert the fiat
priceto a settlement-tokenamountvia an Exodus-minted reference quote instead: same response shape, butprice_lock_codecarries arefq_-prefixed code with a 120-second TTL. A reference quote doesn’t commit anyone to a conversion, it only proves the chargedamountmatches what the subscriber was shown. Funds settle in the token the subscriber paid with, nothing converts, and no fiat settlement configuration is needed.
The subscription id is the on-chain bytes32 hex id — obtain it from GET /merchants/:merchantId/subscriptions, the subscription.created webhook, or the subscription_checkout.completed webhook.
On fiat-priced plans, the quote binds the price_lock_code to the exact amount it returned,
to this subscription, and to the subscription’s token and chain, whether it’s a provider-minted
price lock or an Exodus-minted reference quote. Each code is redeemed once.
POST /subscriptions/:id/charge rejects a mismatch with a quote code; re-quote and re-sign after
any of them. Token-priced plans (price_currency: null) are not quote-bound: a
price_lock_code sent with such a charge is ignored, and none of these codes can fire.
Variable Charges
A fiat-priced plan doesn’t have to charge price every cycle. Pass the amount you want to charge this cycle as amount in the quote body, then sign and submit the token amount the quote returns. Each quote is bound to the amount it converted, so a new amount always means a new quote.
{ "amount": "349000" }{
"object": "subscription_charge_quote",
"subscription_id": "0x9f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a",
"amount": "2406897",
"price_lock_code": "refq_9f3a2b7c4d1e8a05b6c7d2f1",
"rate": "1450",
"expires_at": "2026-09-10T15:04:12Z"
}import { CheckoutSigner } from '@exodus/checkout-signer';
const headers = { Authorization: `Bearer ${process.env.API_KEY}` };
const signer = new CheckoutSigner();
// The subscription's on-chain id, from the subscription_checkout.completed webhook
const subscriptionId = '0x9f3a2b...';
const sub = await fetch(`https://checkout-api.exodus.com/subscriptions/${subscriptionId}`, {
headers,
}).then((r) => r.json());
// Quote this cycle's amount in fiat minor units — ARS 3,490.00 here
const quote = await fetch(`https://checkout-api.exodus.com/subscriptions/${sub.id}/charge-quote`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: '349000' }),
}).then((r) => r.json());
// Sign the quoted token amount, not the fiat amount
const { signature } = signer.signCharge(sub, { amount: BigInt(quote.amount) });
// Submit the same token amount with the quote code before expires_at
await fetch(`https://checkout-api.exodus.com/subscriptions/${sub.id}/charge`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json', 'X-Signature': signature },
body: JSON.stringify({ amount: quote.amount, price_lock_code: quote.price_lock_code }),
});Every quote is checked against the subscriber’s ceilings before any funds move, ad-hoc, stored-price and token-priced alike. The quote rejects with amount_exceeds_cap when the token amount it would return is above cap_amount, so a stored-price quote whose conversion drifts over the cap is rejected too. It rejects with budget_exceeded when that amount would push spent_this_period past budget, or when the window is already fully spent. The charge re-checks both, since spend can accrue between quote and charge. Both ceilings are fixed by the subscriber at subscribe time and the merchant can never raise them, so a plan that expects to reprice needs cap and budget headroom declared on the subscription checkout up front.
Quote errors on charge
The quote binding rejections (quote_not_found, quote_expired, quote_amount_mismatch, quote_asset_mismatch, quote_already_used) and the 422 for a fiat-priced charge sent without a price_lock_code are returned by POST /subscriptions/:id/charge, and that page’s Responses table documents each with its status and meaning. This page keeps only the retry guidance the reference table cannot carry.
A charge that fails after the quote was accepted (a typed simulation failure or an on-chain revert) usually releases the quote, so a retry can resubmit the same amount, price_lock_code, and signature until expires_at. If the retry is rejected with a quote error, re-quote and re-sign.
Errors on this endpoint
| Status | Type | Description |
|---|---|---|
| 400 | invalid_request | Fiat-priced plan on a fiat-settled business without a fiat settlement configuration (“Charge quote is only available for fiat settlements”). |
| 400 | validation_error | amount is present but isn’t a positive integer string with no leading zeros, or the body carries an unrecognized key; a misspelled amount fails loudly rather than silently quoting the stored price. |
| 422 | invalid_request | price_currency_not_quotable — fiat-priced plan on a crypto-settled business, but price_currency isn’t a quotable currency (currently only ARS is). |
| 422 | invalid_request | amount_exceeds_cap — the token amount the quote would return, converted from an ad-hoc amount or from the stored price, exceeds the subscription’s cap_amount. error.data carries subscription_id and cap_amount. |
| 422 | invalid_request | budget_exceeded — the quoted amount would push spent_this_period past the per-window budget, or the window is already fully spent. error.data carries subscription_id, spent_this_period and budget. |
| 422 | invalid_request | missing_budget_state — the subscription has no mirrored budget state (started_at or budget unset), so the ceilings can’t be evaluated. error.data carries subscription_id. |
| 422 | invalid_request | amount_override_not_supported — amount was sent for a token-priced plan, which has no fiat price to override. Sign the token amount directly instead. |
| 404 | not_found | Subscription ID does not exist or belongs to another merchant. |
| 500 | invalid_request | pricing_unconfigured — reference-quote path, the Exodus pricing server isn’t configured. Merchant-side retry won’t help. |
| 502 | invalid_request | Fiat-settled: the settlement provider failed to create the price lock. Crypto-settled: rate_unavailable, the Exodus pricing server didn’t return a usable rate. Retry either case. |
Token-priced plans never fail on settlement configuration — the quote is a passthrough of price.
