Skip to Content
CheckoutAPI ReferenceSubscriptionsOverview

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:

  1. Call POST /subscription-checkouts to create an intent.
  2. Redirect the customer to the returned checkout_url.
  3. The customer signs subscribeAndCharge from their wallet.
  4. The indexer materializes the Subscription and dispatches the subscription.created and subscription.charged webhooks.

Pricing

A plan is priced either directly in the settlement token or in a FIAT currency, set on the originating subscription checkout via price + price_currency:

  • Token-denominated (price_currency omitted) — price is in the settlement token’s smallest units; every cycle charges exactly price.
  • FIAT-denominated (price_currency an ISO 4217 code, e.g. ARS) — price is in the currency’s minor units. Each cycle re-rates to the settlement token at the live rate, so the subscriber pays the current FIAT-equivalent amount, bounded by cap_amount.

The per-cycle amount is not stored on the subscription. Before each charge the merchant fetches the current amount from POST /subscriptions/:id/charge-quote, signs it, and submits.

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.

ActionSigner helperEndpoint
Charge a subscriber for a cyclesignChargePOST /subscriptions/:id/charge
Cancel a subscriptionsignCancelSubscriptionPOST /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.

Plan upgrades and downgrades (raising the cap, with optional proration) are subscriber-authorized — the subscriber consents on the hosted page (modifySubscription). A merchant key can only charge within the subscriber’s cap and cancel; it can never raise the cap or pull off-cycle.

Customer-Initiated Cancel

Customers cancel their own subscriptions from a hosted page:

https://checkout.exodus-int.com/cancel/:subscription_id

The 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

StatusDescription
activeSubscription is live. Eligible for charge once next_charge_at has elapsed.
cancellingCancel transaction has been submitted on-chain and confirmed. No further charges are eligible. Transitions to cancelled once the indexer ingests the SubscriptionCancelled event.
cancelledSubscription cancelled on-chain. No further charges possible.

Paused (in-band sub-state of active)

Subscriptions also carry a separate paused: boolean field — an in-band brake on the active state, not a status enum value. The Exodus scheduler skips charges while paused is true.

TriggerEffect
charge reverts pre-flight with InsufficientBalance or InsufficientAllowance (subscriber fault)paused = true
charge receipt reverts on-chain (any code)paused = true
Next successful charge confirms on-chainpaused = false

paused is visible on every GET /subscriptions/:id and GET /subscriptions response. Surface it in your dunning UI alongside the failed-charge ledger.

Cap Semantics

Every subscription has a cap_amount — the on-chain maximum the contract will allow per call. It is set by the customer when they subscribe and cannot be raised by the merchant — only the subscriber can change it, via the hosted upgrade flow (modifySubscription) or a standalone cap change.

  • charge enforces amount <= cap_amount per call. A cycle whose amount — or, for FIAT plans, the re-rated settlement-token amount — exceeds the cap reverts with ChargeAmountExceedsCap until the subscriber raises the cap.
  • The cap is the subscriber’s security dial: a price increase past it reverts charges rather than silently pulling more.

A merchant who wants headroom for future price increases should pass a generous recommended_cap (a multiple of price) at intent creation — the customer sets the actual cap_amount when they subscribe, seeded by that suggestion.

Failed Charges

The Exodus API persists every charge that reached the chain — succeeded or receipt-reverted — in the Subscription Charges ledger. Pre-flight reverts (the contract rejected the simulation before submission) return a 4xx response and are not persisted. Common typed contract errors:

failure_reasonCause
InsufficientBalanceCustomer’s wallet balance is below amount.
InsufficientAllowanceCustomer revoked the ERC-20 allowance to the SubscriptionManager.
ChargeAmountExceedsCapAttempted to charge more than cap_amount.
PeriodNotElapsedTried to call charge before next_charge_at elapsed.
SubscriptionNotActiveSubscription is no longer active on-chain.

Only succeeded charges fire the subscription.charged webhook. Failed attempts that reached the chain are visible via GET /merchants/:id/charges with status=failed — poll this endpoint for dunning and analytics rather than relying on a webhook for failures.

Available Endpoints

See also: Subscription Checkouts for the intent that creates a subscription, and Subscription Charges for the per-charge ledger.

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