Quote the next charge for a subscription
POST/subscriptions/{subscriptionId}/charge-quoteQuote the next charge for a subscription. Crypto-priced plans return the fixed amount; fiat-priced plans return a locked rate and a price_lock_code to submit with the charge. Fiat-priced plans may pass an optional amount in fiat minor units to quote an ad-hoc charge; omitted, the stored plan price is quoted. Every fiat quote is checked against the subscription's cap_amount and the remaining per-window budget at quote time.
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-int.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 | |
| amount | string | yes | |
| price_lock_code | string | null | yes | |
| rate | string | null | yes | |
| expires_at | string | null | yes |
Example Response
{
"object": "subscription_charge_quote",
"subscription_id": "0x9f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a",
"amount": "9990000",
"price_lock_code": null,
"rate": null,
"expires_at": null
}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 one of the codes below; 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-int.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-int.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-int.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
| Status | Code | Meaning |
|---|---|---|
| 422 | invalid_request | No price_lock_code was sent on a fiat-priced subscription (“price_lock_code is required for fiat-priced subscription charges”). This one carries no code field and fires before the binding checks below. |
| 422 | quote_not_found | No quote with this price_lock_code exists for this subscription. A code minted for a different subscription lands here too. |
| 409 | quote_expired | The quote’s expires_at has passed. |
| 422 | quote_amount_mismatch | The charge amount differs from the quoted amount. |
| 422 | quote_asset_mismatch | The quote’s token or chain doesn’t match the charge. Rare on this path: token and chain are read off the subscription, so a code minted for a different subscription returns quote_not_found instead. |
| 409 | quote_already_used | The quote has already been reserved or consumed by another request. Fetch a new quote. |
A charge that fails after the quote was accepted (insufficient balance, an on-chain revert, any error past the binding checks) releases the quote. The same price_lock_code stays valid until expires_at, so a retry can resubmit the same amount, price_lock_code, and signature without re-quoting. Only the codes above require a fresh quote; if a retry comes back with quote_already_used, treat it the same way.
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.
