Charge a Subscription
POST/subscriptions/:id/chargeDescription
Submit a recurring cycle charge against an active subscription. The amount charged is the subscription’s fixed charge_amount (settlement-token smallest units, set when the subscription was created). You sign that amount together with the current charge_nonce; Exodus then submits the on-chain SubscriptionManager.charge() transaction and pays the gas.
A successful call advances the on-chain period clock; last_charged_at and next_charge_at are updated by the indexer once the transaction confirms.
Succeeded charges are persisted in the Subscription Charges
ledger and dispatched via the subscription.charge_succeeded webhook. Contract reverts return a
4xx response (see Errors) and do not fire the success webhook; receipts that
reverted on-chain are still persisted in the ledger with status: "failed" and a typed
failure_reason, and dispatched via subscription.charge_failed. Pre-flight reverts (rejected
before submission) are not persisted.
Subscriptions today are token-denominated: charge_amount is a fixed settlement-token amount.
FIAT-denominated plans — where the cycle amount is converted from a fiat price at charge time —
will soon be enabled in production environments.
Headers
| Header | Description | Required |
|---|---|---|
| Authorization | Bearer token with your API key | yes |
| X-Signature | Signature from `signCharge(...)` | yes |
Path Parameters
| Name | Type | Description | Required |
|---|---|---|---|
| id | string | The on-chain subscription ID (bytes32 hex, e.g. `0x9f3a...`). Obtain it from `GET /subscriptions`, the `subscription.created` webhook, or the `subscription_checkout.completed` webhook. | yes |
Request Body
No request body. The charge amount is the subscription’s stored charge_amount; your X-Signature (from signCharge) authorizes charging that exact amount at the current charge_nonce. If the subscription’s charge_amount has changed, fetch it again from GET /subscriptions/:id and re-sign before submitting.
Charging a cycle
import { CheckoutSigner } from '@exodus/checkout-signer';
const id = '0x9f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a';
const headers = { Authorization: `Bearer ${process.env.API_KEY}` };
const signer = new CheckoutSigner();
// 1. Read the subscription
const sub = await fetch(`https://checkout-api.exodus-int.com/subscriptions/${id}`, {
headers,
}).then((r) => r.json());
// 2. Sign the charge — the client reads everything it needs off the subscription
const { signature } = signer.signCharge(sub);
// 3. Submit the signature (no request body)
const response = await fetch(`https://checkout-api.exodus-int.com/subscriptions/${id}/charge`, {
method: 'POST',
headers: {
...headers,
'X-Signature': signature,
},
});
const charge = await response.json();
console.log(charge.subscription_id, charge.status, charge.tx_hash);Response
{
"subscription_id": "0x9f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a",
"charge_nonce": 3,
"amount": "9990000",
"tx_hash": "0xabc...",
"status": "succeeded"
}The full per-charge ledger row — including subscriber, chain, fee, charged_at, and failure_reason on failures — is written to the Subscription Charges collection and dispatched via the subscription.charge_succeeded / subscription.charge_failed webhook.
Errors
The API rejects calls with a structured error response ({ error: { type, code?, message, data? } }). Errors fall into two categories.
API-side preflight rejections
Returned before any on-chain submission, so no gas is spent.
| Code | Meaning |
|---|---|
subscription_cancelled | Subscription is cancelled or cancelling. |
period_not_elapsed | next_charge_at > now. Response data includes next_charge_at. |
Signature failures return error.type: "authentication_error" (no code). Malformed X-Signature headers return error.type: "validation_error".
Contract reverts
If the on-chain charge() call reverts (either pre-flight simulation or after the receipt), the API returns a 400 with error.code set to the decoded revert name. Subscriber-fault reverts (InsufficientBalance, InsufficientAllowance) also flip subscription.paused = true so the scheduler skips further attempts.
code | Cause |
|---|---|
InsufficientBalance | Subscriber’s wallet balance is below charge_amount. |
InsufficientAllowance | Subscriber revoked the ERC-20 allowance. |
ChargeAmountExceedsCap | charge_amount > cap_amount. |
PeriodNotElapsed | On-chain block time was earlier than next_charge_at (race with preflight). |
SubscriptionNotActive | Subscription is no longer active on-chain. |
OnChainRevert | Receipt reverted with an unrecognized code. The failed attempt is persisted to the ledger row. |
Read-your-writes guarantee. When the subscription.charge_succeeded webhook fires, the
corresponding SubscriptionCharge row is durable. An immediate GET /charges with no cursor or
with starting_after pointing to a row older than the new charge is guaranteed to include the new
charge.
