Skip to Content

Charge a Subscription

POST/subscriptions/:id/charge

Description

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

HeaderDescriptionRequired
AuthorizationBearer token with your API keyyes
X-SignatureSignature from `signCharge(...)`yes

Path Parameters

NameTypeDescriptionRequired
idstringThe 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

charge-subscription.js
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

SUCCESSFUL CHARGE
{
  "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.

CodeMeaning
subscription_cancelledSubscription is cancelled or cancelling.
period_not_elapsednext_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.

codeCause
InsufficientBalanceSubscriber’s wallet balance is below charge_amount.
InsufficientAllowanceSubscriber revoked the ERC-20 allowance.
ChargeAmountExceedsCapcharge_amount > cap_amount.
PeriodNotElapsedOn-chain block time was earlier than next_charge_at (race with preflight).
SubscriptionNotActiveSubscription is no longer active on-chain.
OnChainRevertReceipt 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.

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