Charge Ad-Hoc
POST/subscriptions/:id/charge-adhocDescription
Submit an off-cycle one-shot charge against an active subscription. Use this for usage true-ups, proration deltas, late fees, or any charge that should NOT advance the recurring period clock.
Key differences from POST /subscriptions/:id/charge:
- Does not update
last_charged_atornext_charge_at. The recurring schedule continues unchanged. - Shares the same
charge_noncecounter with cycle charges (one global counter per subscription), so signatures are non-fungible across the two actions. - Subject to the same per-call cap (
amount <= cap_amount). Cap is not cumulative across calls within a period.
Sign with signChargeAdHoc from @exodus/checkout-signer and pass the signature in the X-Signature header.
The hosted proration UX wrapper that orchestrates an ad-hoc charge plus an
updateChargeAmount together for true plan upgrades is not in v1 — schedule the two API calls
yourself if you need that flow.
Headers
| Header | Description | Required |
|---|---|---|
| Authorization | Bearer token with your API key | yes |
| Content-Type | application/json | yes |
| X-Signature | Signature from `signChargeAdHoc(...)` | yes |
Path Parameters
| Name | Type | Description | Required |
|---|---|---|---|
| id | string | The subscription ID (e.g. `sub_abc123def456`). | yes |
Body Parameters
| Name | Type | Description | Required |
|---|---|---|---|
| amount | string | Off-cycle amount in token's smallest unit. MUST equal the value passed to `signChargeAdHoc`. MUST be `> 0` and `<= cap_amount`. | yes |
| charge_nonce | number | Pre-increment `charge_nonce` from the latest `GET /subscriptions/:id` read. MUST equal the value passed to `signChargeAdHoc`. | yes |
Producing the Signature
import { signChargeAdHoc } from '@exodus/checkout-signer'
const sub = await fetch('https://checkout.exodus.com/subscriptions/sub_abc123def456', {
headers: { Authorization: `Bearer ${process.env.API_KEY}` },
}).then((r) => r.json())
const amount = '1500000' // $1.50 USDC proration
const signature = signChargeAdHoc(
sub.onchain_id,
amount,
sub.charge_nonce,
sub.subscription_manager_address,
sub.chain,
process.env.SIGNING_PRIVATE_KEY,
)
const response = await fetch(
`https://checkout.exodus.com/subscriptions/${sub.id}/charge-adhoc`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.API_KEY}`,
'Content-Type': 'application/json',
'X-Signature': signature,
},
body: JSON.stringify({
amount,
charge_nonce: sub.charge_nonce,
}),
},
)
const charge = await response.json()
console.log(charge.id, charge.kind) // "subc_...", "adhoc"Response (succeeded)
{
"object": "subscription_charge",
"id": "subc_adhoc1234abcdef",
"subscription_id": "sub_abc123def456",
"subscriber": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE21",
"amount": "1500000",
"fee": "0",
"tx_hash": "0xabc...",
"chain": "eip155:1",
"charge_nonce": 4,
"charged_at": "2026-05-15T09:30:00Z",
"status": "succeeded",
"kind": "adhoc",
"failure_reason": null
}Note that charge_nonce advanced but last_charged_at and next_charge_at on the parent Subscription are unchanged.
Errors
API-side preflight rejections
| Code | Meaning |
|---|---|
subscription_cancelled | Subscription is cancelled or cancelling. |
charge_amount_exceeds_cap | amount > cap_amount. Cap is enforced per-call. |
invalid_signature | Signature does not recover to your registered signing address. |
nonce_mismatch | charge_nonce is stale. Re-fetch and re-sign. |
Contract reverts (decoded from receipt)
Same set as the cycle charge endpoint except PeriodNotElapsed does not apply (ad-hoc ignores the period clock).
Webhook Fired
subscription.charged_ad_hoc — payload mirrors subscription.charged with kind: "adhoc".
