Subscriptions
A Subscription is the on-chain record of an active recurring stablecoin agreement. It is materialized by the indexer when a customer completes a subscription checkout and the first subscribeAndCharge transaction confirms on-chain.
Creation
Subscriptions are not created via a direct API call. To start one:
- Call
POST /subscription-checkoutsto create an intent. - Redirect the customer to the returned
checkout_url. - The customer signs
subscribeAndChargefrom their wallet. - The indexer materializes the
Subscriptionand dispatches thesubscription.createdandsubscription.chargedwebhooks.
Merchant-Driven Actions
After creation, the merchant drives all subsequent state changes via the API. Each action is a signed request: the merchant uses @exodus/checkout-signer to produce a signature, posts it to the Exodus API, and Exodus submits the on-chain transaction.
| Action | Signer helper | Endpoint |
|---|---|---|
| Charge a subscriber for a scheduled cycle | signCharge | POST /subscriptions/:id/charge |
| Charge an off-cycle one-shot amount | signChargeAdHoc | POST /subscriptions/:id/charge-adhoc |
| Change the recurring price | signUpdateChargeAmount | POST /subscriptions/:id/update-charge-amount |
| Cancel a subscription | signCancelSubscription | POST /subscriptions/:id/cancel |
All merchant actions require a signing key. See Signed Requests for setup, and the per-action pages for the exact signer call and request body.
Customer-Initiated Cancel
Customers cancel their own subscriptions from a hosted page:
https://checkout.exodus.com/cancel/:subscription_idThe page reads the on-chain subscription_id, requires the customer’s subscribing wallet to be connected, and lets them sign cancel(subscriptionId) directly. Surface this URL in your customer account UI for self-service cancellation. The customer pays the on-chain gas.
If the customer has lost access to their subscribing wallet, the page surfaces a “Lost wallet?” link. Exodus does not perform key recovery — recovery routes back to the merchant via the intent’s cancel_url, and the merchant cancels via POST /subscriptions/:id/cancel.
Statuses
| Status | Description |
|---|---|
active | Subscription is live. Eligible for charge once next_charge_at has elapsed; eligible for charge_adhoc at any time. |
cancelling | Off-chain honor flag set via cancel_at_period_end on a cancel call. Contract still allows charges; the merchant scheduler skips them. Transitions to cancelled when the on-chain cancelWithSig is submitted (or when the customer cancels directly). |
cancelled | Subscription cancelled on-chain. No further charges possible. |
cancelling is honor-system off-chain. The on-chain contract has no concept of “scheduled to
cancel” — it only knows active and cancelled. The Exodus scheduler honors the flag and skips
scheduled charges; merchants self-submitting from their own RPC must honor it themselves.
Cap Semantics
Every subscription has a cap_amount — the on-chain maximum the contract will allow per call. It is set once when the customer subscribes and cannot be raised by the merchant (the customer would have to re-sign a new subscription).
chargeenforcesamount <= cap_amountper call.charge_adhocenforcesamount <= cap_amountper call (no cumulative tracking).update_charge_amountenforcesnew_amount <= cap_amount. Reverts withChargeAmountExceedsCapotherwise.
A merchant who wants headroom for future price increases should set cap_amount to a multiple of charge_amount at intent creation.
Failed Charges
The Exodus API submits charges and persists the outcome of every attempt — both succeeded and failed — in the Subscription Charges ledger. Common typed contract errors:
failure_reason | Cause |
|---|---|
InsufficientBalance | Customer’s wallet balance is below amount. |
InsufficientAllowance | Customer revoked the ERC-20 allowance to the SubscriptionManager. |
ChargeAmountExceedsCap | Attempted to charge more than cap_amount. |
PeriodNotElapsed | Tried to call charge before next_charge_at elapsed. |
SubscriptionNotFound | Subscription does not exist on the contract. |
SubscriptionAlreadyCancelled | Tried to charge a cancelled subscription. |
Failed scheduled charges fire the subscription.charged webhook with data.object.status: "failed" and the typed failure_reason. Use the GET /merchants/:id/charges endpoint with status=failed for analytics and dunning workflows.
Available Endpoints
- Get Subscription — Retrieve subscription details
- List Subscriptions — List all subscriptions
- Charge a Subscription — Submit a scheduled charge
- Charge Ad-Hoc — Submit an off-cycle charge
- Update Charge Amount — Change the recurring price
- Cancel Subscription — Cancel a subscription as the merchant
See also: Subscription Checkouts for the intent that creates a subscription, and Subscription Charges for the per-charge ledger.
