Skip to Content

Charge a subscription

POST/subscriptions/{subscriptionId}/charge

Charge an active subscription. Sign the charge amount together with the subscription's current charge_nonce; Exodus submits the on-chain charge and pays the gas. Quote first: amount is the token amount the charge quote returned and price_lock_code is its code. Each charge is bounded by the per-call cap_amount and the per-cycle budget, so multiple charges may run within a period until the budget is exhausted. Succeeded charges are recorded in the charges ledger and dispatched via the subscription.charge_succeeded webhook.

Headers

HeaderDescriptionRequired
AuthorizationBearer token with your secret API keyyes

Path Parameters

NameTypeRequiredDescription
subscriptionIdstringyes

Request Body

FieldTypeRequiredDescription
amountstringyesThe amount to charge, in the settlement token's smallest units, identical to the amount signed. This is the amount the charge quote returned, verbatim. On a legacy token-denominated plan (no price_currency) it is any amount within cap_amount and the remaining per-window budget.
price_lock_codestring—The price_lock_code of the charge quote whose amount is being charged. Required on fiat-priced plans, which is every plan that can be created today; the schema keeps it optional only for legacy token-denominated plans. The quote is bound to this subscription and to amount, and a missing, mismatched, expired or already-used code is rejected. A code minted for another subscription is reported as not found. Ignored on a legacy token-denominated plan.
metadataobject—Flat map of merchant-defined references. Up to 50 keys; keys up to 40 characters; values are strings up to 500 characters (nested values are rejected).

Example Request

Request

const response = await fetch('https://checkout-api.exodus-int.com/subscriptions/<subscriptionId>/charge', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer sk_live_xxxxxxxxxxxxxxxx',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "amount": "9990000",
    "price_lock_code": "refq_3f9a1c7d2e5b8064",
    "metadata": {
      "invoice_id": "inv_202605"
    }
  }),
});

Responses

StatusDescription
200The charge result — `status` is always `succeeded` on a 200 (failures throw error responses). Fetch the subscription for the updated budget and scheduling state.
400Validation error
401Missing or invalid API key, missing `X-Signature` header, or the `X-Signature` was not produced by the merchant signer registered for the subscription's chain ("Invalid merchant signature"). A present but wrong-size `X-Signature` is rejected as a 400 with `param: X-Signature`; it must be a 64-byte (Solana) or 65-byte (EVM) hex string.
403API key lacks the required scope
404Resource not found
409The subscription or the quote is in a state that refuses the charge. `subscription_cancelled`: the subscription is `cancelled` or `cancelling`. `quote_expired`: the `price_lock_code` passed its `expires_at`. `quote_already_used`: the `price_lock_code` was already reserved or consumed by another request. Re-quote and re-sign for either quote code.
422The charge cannot be processed as sent. Configuration and request-shape rejections carry no `code` (`type: invalid_request`): the message starts with `price_currency_not_quotable:`, fiat settlement not configured, `price_currency` differs from the settlement currency, `price_lock_code` missing on a fiat-priced plan, merchant signer not configured. Ceilings: `amount_exceeds_cap`, `budget_exceeded`, `missing_budget_state`. Quote binding: `quote_amount_mismatch`, `quote_asset_mismatch`, `quote_not_found`; re-quote and re-sign. Compliance screening (`type: cannot_process`): `flagged`, either because the subscriber failed screening or because the merchant's settlement address is under a compliance hold after an out-of-band rotation; the hold clears when the merchant rotates to a different address that passes screening. Revert decoded before submission: `code` is the contract error name (`BudgetExceeded`, `ChargeAmountExceedsCap`, `ChargeAmountExceedsBudget`, `InsufficientBalance`, `InsufficientAllowance`, `SubscriptionNotActive`, `SubscriptionNotFound`, `InvalidSignature`, `InvalidNonce`, `SignatureExpired`); the failed charge is recorded in the charges ledger. A failed charge consumes no `charge_nonce` and no budget, so the subscription stays chargeable; retry once the cause is resolved. Post-submission revert on EVM, and the Solana program errors the API recognises: `OnChainRevert`; the failed charge is recorded with its `tx_hash`. Any other chain error returns the 500 described below.
500Relayer wallet not configured on this environment (no `code`); the quote is released, so once the environment is fixed the same `amount`, `price_lock_code` and signature stay valid until `expires_at`. `charge_persist_failed`: the charge landed on-chain but its receipt could not be recorded; `data` carries `subscription_id`, `charge_nonce` and `tx_hash`. Do not resubmit: the charge appears once indexed, and metadata can be attached afterwards through the charge metadata endpoint. Any other unexpected failure inside the charge relay, including a chain error the API cannot decode, returns "Internal server error" with no `code`; the quote is not released on that path, so re-quote before retrying.
503`type: service_unavailable`. No `code`: the relayer is paused by operators or, on EVM, cannot pay gas right now, or the merchant's settlement address is awaiting a compliance re-screen after an out-of-band rotation, or the address the merchant's payment factory holds on-chain differs from the one Exodus has screened (message `The settlement address on this chain differs from the one Exodus has screened`; the relay refuses until the rotation monitor screens it or the merchant rotates that chain through the settlement endpoint), or the subscription manager points at a payment factory that is neither the screened one nor the target of a signer rotation in progress (message `The subscription manager on this chain points at a payment factory Exodus has not screened`; retry the signer rotation or rotate the manager back), or one of those on-chain reads failed (message `Settlement address check is temporarily unavailable`; retry later). The quote is released, so retry later with the same `amount`, `price_lock_code` and signature until `expires_at`. `code: subscription_manager_paused`: the merchant's subscription manager is paused on-chain; retrying will not succeed until the manager's pauser unpauses it. The pause may be Exodus-initiated as an incident response, so confirm the cause with support before unpausing.

Response body

FieldTypeRequiredDescription
subscription_idstringyes
charge_noncenumberyes
amountstringyes
tx_hashstringyes
statusenum: succeededyes
metadataobject | nullyes

Example Response

{
  "subscription_id": "0x9f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a",
  "charge_nonce": 3,
  "amount": "9990000",
  "tx_hash": "0x7e81c228fc4fcf0372a86ce6f8bbc457210a8b51fc30dee5557280e5153c9f06",
  "status": "succeeded",
  "metadata": {
    "invoice_id": "inv_202605"
  }
}

Error Response

Error (e.g. 400)

{
  "error": {
    "type": "validation_error",
    "message": "amount must be a positive number",
    "param": "amount"
  }
}
Last updated on

Start building

XO

Request Demo

Schedule a call with our team

Select a product
Arrow right

Start building
Grateful

Contact Us

We're here to help