Subscription Charges
A SubscriptionCharge is the per-attempt record of a charge against a Subscription. Both succeeded and failed charges are persisted — failed attempts include a typed failure_reason for dunning, analytics, and CSV exports.
What’s in a Charge
| Field | Description |
|---|---|
id | subc_... resource id, stable across redeliveries. |
subscription_id | The parent subscription. |
subscriber | On-chain subscriber address (denormalized for filtering). |
amount | Charged amount in token’s smallest unit. For failed charges this is the attempted amount. |
fee | Network fee deducted, in token’s smallest unit. null on failed charges that did not settle on-chain. |
tx_hash | On-chain transaction hash. null only when the transaction was never broadcast (rare — preflight rejections are NOT persisted here). |
chain | CAIP-2 identifier (e.g. eip155:1). |
charge_nonce | Monotonic per-subscription counter. 0 for the first charge from subscribeAndCharge. Shared between cycle and ad-hoc charges. |
charged_at | Block timestamp on success; API submission time on failure. |
status | succeeded or failed. |
kind | cycle (from POST /subscriptions/:id/charge) or adhoc (from POST /subscriptions/:id/charge-adhoc). |
failure_reason | Typed contract error name on failure (e.g. InsufficientBalance, ChargeAmountExceedsCap, PeriodNotElapsed). null on success. |
Sources of Truth
The same charge can be written by two paths:
- API write path — when a merchant calls
POST /subscriptions/:id/chargeor/charge-adhoc, Exodus submits the transaction and persists the receipt synchronously. - Indexer backup path — when a merchant submits directly from their own RPC, the indexer ingests the
SubscriptionCharged/SubscriptionChargedAdHocevent and inserts a row.
Rows are deduplicated by tx_hash (unique). Cursor pagination (see List Subscription Charges) uses an internal monotonic sequence, so even if a row arrives via the indexer’s late backfill it lands at the head of the list without disturbing already-paged rows.
Read-Your-Writes
After receiving a subscription.charged webhook, the corresponding row is durable in the ledger. An immediate GET /merchants/:id/charges with no cursor — or with starting_after pointing to a row strictly older than the new charge — is guaranteed to include it.
API-side preflight rejections are NOT persisted as charges. When the API rejects a call with
period_not_elapsed, charge_amount_exceeds_cap, invalid_signature, nonce_mismatch, or
subscription_cancelled, no on-chain transaction is submitted and no SubscriptionCharge row is
written. Track these in your scheduler logs.
Available Endpoints
- List Subscription Charges — Filter by subscription, subscriber, date range, status, chain.
