Create Subscription Checkout
POST/subscription-checkoutsDescription
Create a single-use intent that authorizes a customer to start a recurring subscription. The response includes a checkout_url — redirect the customer there to complete the subscribe + first-charge flow.
The intent does not require a signed request at creation time. The customer’s on-chain signature at the hosted page authorizes the subscription and the first charge atomically. Subsequent merchant-driven charges and cancels use signed requests.
Headers
| Header | Description | Required |
|---|---|---|
| Authorization | Bearer token with your API key | yes |
| Content-Type | application/json | yes |
Body Parameters
The merchant declares the plan limits — price, budget, and optionally cap. The subscriber signs these exact values at the hosted page; they are not customer-editable.
| Name | Type | Description | Required |
|---|---|---|---|
| token_symbol | string | Settlement token the subscriber pays in (e.g. "USDC", "USDT"). If provided, must resolve to a token address on every chain in supported_chains, with consistent decimals across them. Omit to offer every stablecoin configured on the supported chains — the customer picks the token at sign time. | no |
| price | string | Recurring per-cycle amount, as an integer string in the settlement token's smallest unit (e.g. "9990000" = 9.99 USDC). | yes |
| price_currency | string | Fiat currency to denominate the plan in ("ARS", "BRL", "USD"). Requires a fiat settlement configuration on your business — without one, fiat-priced intents are accepted but cannot be quoted or charged (quotes fail with 400, charges with 422). When set, price is in fiat minor units (e.g. "999" = $9.99) and each cycle re-rates to the settlement token at the live rate; budget and cap stay in token smallest units. Omit to price the subscription in the settlement token. | no |
| budget | string | Per-cycle spend ceiling, as an integer string in the settlement token's smallest unit — the maximum total that can be charged within a single billing window (supports multiple metered charges per cycle). Must be >= price. | yes |
| cap | string | Optional per-charge maximum, in the settlement token's smallest unit — the most a single charge can pull. Must be >= price and <= budget. Defaults to budget when omitted. | no |
| period_duration | number | Seconds between scheduled charges. Minimum 3600 (1 hour). | yes |
| supported_chains | string[] | CAIP-2 chain identifiers the customer can pick from (e.g. ["eip155:1", "eip155:137"]). Defaults to every chain where the merchant has a SubscriptionManager deployment. V1 supports the eip155: namespace only. | no |
| subscriber | string | Pre-bind the intent to a specific EVM wallet address. When set, the hosted page rejects any other wallet. | no |
| external_customer_id | string | Your customer reference. Indexable for filtered queries (GET /subscription-checkouts?external_customer_id=...). | no |
| success_url | string | URL the hosted subscribe page redirects to after the first charge confirms. The page appends ?subscription_checkout_id=schk_xxx&subscription_id=0x... (the on-chain subscription ID). | no |
| cancel_url | string | URL surfaced when the customer dismisses the hosted page or follows a "lost wallet" recovery link. | no |
| metadata | object | Arbitrary JSON object for your own bookkeeping (plan reference, session id, referral code, etc.). Echoed back on the intent and copied to the resulting Subscription. | no |
| expires_at | string | ISO 8601 expiry timestamp. Must be in the future and within 24 hours. Defaults to 5 minutes from creation. | no |
Example Request
const response = await fetch('https://checkout-api.exodus-int.com/subscription-checkouts', {
method: 'POST',
headers: {
Authorization: 'Bearer sk_live_xxxxxxxxxxxxxxxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
token_symbol: 'USDC',
price: '9990000',
budget: '300000000',
cap: '120000000',
period_duration: 2592000,
supported_chains: ['eip155:1', 'eip155:137', 'eip155:42161'],
external_customer_id: 'cus_42',
success_url: 'https://merchant.com/subscribed',
cancel_url: 'https://merchant.com/cancelled',
metadata: { external_plan_ref: 'pro_monthly' },
}),
});
const intent = await response.json();
window.location.href = intent.checkout_url;FIAT-denominated pricing — pricing the subscription in a fiat currency via price_currency and re-rating to the settlement token at each cycle’s live rate — is available only to businesses with a fiat settlement configuration. Without one, fiat-priced intents are accepted at creation but cannot be completed: charge quotes fail with 400 invalid_request and charges fail with 422. Price in a settlement token unless fiat settlement is set up on your account.
Response
{
"object": "subscription_checkout",
"id": "schk_1234567890abcdef",
"status": "pending",
"onchain_id": "0x9f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a",
"business_name": "Acme Inc",
"subscriber": null,
"external_customer_id": "cus_42",
"subscription_options": [
{ "chain": "eip155:1", "token": "USDC", "subscription_manager_address": "0xa1b2c3d4e5f6789012345678901234567890abcd", "relayer_address": "0xd4e5f6a7b8c90123456789012345678901234ef1" },
{ "chain": "eip155:137", "token": "USDC", "subscription_manager_address": "0xb2c3d4e5f67890123456789012345678901234cd", "relayer_address": "0xe5f6a7b8c9d012345678901234567890123456f2" },
{ "chain": "eip155:42161", "token": "USDC", "subscription_manager_address": "0xc3d4e5f6789012345678901234567890123456cd", "relayer_address": "0xf6a7b8c9d0e12345678901234567890123456af3" }
],
"price": "9990000",
"price_currency": null,
"period_duration": 2592000,
"cap": "120000000",
"budget": "300000000",
"checkout_url": "https://checkout.exodus-int.com/subscribe/schk_1234567890abcdef",
"success_url": "https://merchant.com/subscribed",
"cancel_url": "https://merchant.com/cancelled",
"metadata": { "external_plan_ref": "pro_monthly" },
"expires_at": "2026-05-19T12:05:00Z",
"created_at": "2026-05-19T12:00:00Z",
"updated_at": "2026-05-19T12:00:00Z",
"completed_at": null,
"cancelled_at": null,
"expired_at": null,
"cancellation_reason": null
}onchain_id is the on-chain subscriptionId (CSPRNG-minted at creation) the contract will identify this subscription by. subscription_options lists every (chain, token, subscription_manager_address, relayer_address) the subscriber can pick from at sign time — one row per chain×token offered; each row’s chain is a CAIP-2 identifier, and relayer_address is the Exodus relayer that submits that chain’s transactions. Passing token_symbol limits the rows to that token; omit it to offer every configured stablecoin. cap echoes the effective per-charge maximum — equal to budget when you didn’t set one.
Validation
Validation failures return an error envelope { error: { type, message, param? } }. There is no machine code for these; switch on error.type and surface error.message.
type | Status | Cause |
|---|---|---|
invalid_request | 422 | supported_chains includes a chain where the merchant has no SubscriptionManager deployment. |
invalid_request | 422 | token_symbol doesn’t resolve to a token on every chain in supported_chains. |
validation_error | 400 | price_currency is not one of ARS, BRL, USD. |
validation_error | 400 | budget is below price (budget must be >= price). |
validation_error | 400 | cap is outside price <= cap <= budget. |
validation_error | 400 | period_duration < 3600 (1 hour). |
validation_error | 400 | expires_at is in the past or more than 24 hours in the future. |
validation_error | 400 | A field failed schema validation (e.g. subscriber is not a valid EVM address). param names the field. |
For v1, the chosen token_symbol must have consistent decimals across every chain in
supported_chains so price, cap, and budget are unambiguous. USDC and USDT both
satisfy this on EVM (6 decimals, consistent across chains).
Webhooks Fired
subscription_checkout.created— on successful creationsubscription_checkout.expired— ifexpires_atelapses without a successful subscribesubscription_checkout.completed— when the customer signssubscribeAndChargeand the first charge confirms
See Webhooks for the full event payloads.
