Skip to Content

Charge Ad-Hoc

POST/subscriptions/:id/charge-adhoc

Description

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_at or next_charge_at. The recurring schedule continues unchanged.
  • Shares the same charge_nonce counter 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

HeaderDescriptionRequired
AuthorizationBearer token with your API keyyes
Content-Typeapplication/jsonyes
X-SignatureSignature from `signChargeAdHoc(...)`yes

Path Parameters

NameTypeDescriptionRequired
idstringThe subscription ID (e.g. `sub_abc123def456`).yes

Body Parameters

NameTypeDescriptionRequired
amountstringOff-cycle amount in token's smallest unit. MUST equal the value passed to `signChargeAdHoc`. MUST be `> 0` and `<= cap_amount`.yes
charge_noncenumberPre-increment `charge_nonce` from the latest `GET /subscriptions/:id` read. MUST equal the value passed to `signChargeAdHoc`.yes

Producing the Signature

charge-adhoc.js
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)

AD-HOC CHARGE 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

CodeMeaning
subscription_cancelledSubscription is cancelled or cancelling.
charge_amount_exceeds_capamount > cap_amount. Cap is enforced per-call.
invalid_signatureSignature does not recover to your registered signing address.
nonce_mismatchcharge_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".

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