Quickstart
Start accepting recurring stablecoin payments in just a few minutes.
Prerequisites
Before you begin, you’ll need:
- An Exodus Business account - Contact [email protected] to create your account and complete the onboarding process
- API credentials - Once your account is approved, you’ll find your API keys in your dashboard settings
- Settlement address - Configure the wallet address where you’ll receive stablecoin payments
- Webhook secret - Configure your webhook endpoint URL in the dashboard to receive your webhook signing secret
- A server-side environment - Node.js, Python, Ruby, or any language that can make HTTP requests
New to Exodus Business? Our team will guide you through the onboarding process, including KYB verification and account configuration. Reach out to [email protected] to get started.
Never expose your API key in client-side code. Always make API calls from your server.
Set Up Your Signing Key
Install the signing SDK and generate a key pair:
npm install @exodus/checkout-signerimport { generateSigningKey } from '@exodus/checkout-signer'
const key = generateSigningKey()
console.log('Private key:', key.privateKey) // store securely as SIGNING_PRIVATE_KEY
console.log('Signer address:', key.address) // send to your account managerStore the private key in your environment variables. Send the address to your account manager.
Create a Direct Payment
The simplest payment flow — funds go directly to your settlement address when the customer pays.
import { signDirectPayment } from '@exodus/checkout-signer'
const { onChainId, signature } = signDirectPayment(
process.env.SIGNING_PRIVATE_KEY,
)
const response = await fetch('https://checkout.exodus.com/checkouts', {
method: 'POST',
headers: {
Authorization: 'Bearer sk_test_xxxxxxxxxxxxxxxx',
'Content-Type': 'application/json',
'X-Signature': signature,
},
body: JSON.stringify({
amount: 2999,
currency: 'USD',
payment_method: 'direct',
on_chain_id: onChainId,
description: 'Pro Plan - Monthly',
success_url: 'https://yoursite.com/success',
cancel_url: 'https://yoursite.com/cancel',
}),
})
const checkout = await response.json()
console.log(checkout.checkout_url)Create Your First Subscription
Subscriptions in v1 start with a single-use Subscription Checkout intent. The customer signs an on-chain authorization on the hosted page, and the first charge is atomic with the subscribe — there is no separate “activate” step.
const response = await fetch('https://checkout.exodus.com/subscription-checkouts', {
method: 'POST',
headers: {
Authorization: 'Bearer sk_test_xxxxxxxxxxxxxxxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
token_symbol: 'USDC',
charge_amount: '9990000', // $9.99 USDC (6 decimals)
cap_amount: '120000000', // $120 max per cycle — enforced on-chain forever
period_duration: 2592000, // 30 days in seconds
supported_chains: ['eip155:1', 'eip155:137'], // CAIP-2 — subscriber picks at sign time
external_customer_id: 'cus_42',
success_url: 'https://yoursite.com/welcome',
cancel_url: 'https://yoursite.com/pricing',
metadata: { external_plan_ref: 'pro_monthly' },
}),
})
const intent = await response.json()
// Redirect your customer to intent.hosted_url
console.log(intent.hosted_url) // https://checkout.exodus.com/subscribe/schk_...cap_amount is the on-chain ceiling. Subscribers authorize the contract for any amount up to
cap_amount — raising charge_amount later (via Update Charge
Amount) never requires a new customer
signature. Set a generous cap up front; the contract enforces it forever.
Redirect the Customer
Send your customer to the hosted_url returned in the response:
// After receiving the hosted URL from your server
window.location.href = hostedUrlThe customer will:
- Connect their wallet (Exodus, Grateful, MetaMask, Phantom, etc.)
- Pick the chain to subscribe on (out of
supported_chains) - Sign the on-chain
subscribeAndCharge— first charge clears in the same transaction - Be redirected to your
success_urlonce confirmed
Handle Webhooks
Set up a webhook endpoint to receive subscription events:
import express from 'express'
import crypto from 'crypto'
const app = express()
// Use express.raw() so we can verify the signature over the unparsed body
app.post('/webhooks/payments', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-signature']
const expectedSignature = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body)
.digest('hex')
if (signature !== expectedSignature) {
return res.status(401).send('Invalid signature')
}
const event = JSON.parse(req.body)
switch (event.type) {
case 'subscription_checkout.completed': {
// Customer subscribed AND the first charge cleared on-chain.
// Payload bundles the intent, the subscription, and the first charge.
const { object: intent, subscription, first_charge } = event.data
console.log('Subscription started:', subscription.id)
// Grant access — keyed on intent.external_customer_id ("cus_42")
break
}
case 'subscription.charged': {
const charge = event.data.object
if (charge.status === 'succeeded') {
console.log('Cycle charge succeeded:', charge.id)
} else {
// Failed attempt — `failure_reason` is a typed contract error
// (e.g., "InsufficientBalance"). Drive your dunning flow from here.
console.log('Cycle charge failed:', charge.id, charge.failure_reason)
}
break
}
case 'subscription.cancelled': {
console.log('Subscription cancelled:', event.data.object.id)
// Revoke access
break
}
}
res.status(200).send('OK')
})
app.listen(3000)Charging Subsequent Cycles
Subscriptions don’t auto-charge on a schedule — your scheduler decides when to call the API. For each cycle, sign a charge with your private key and POST it:
import { signCharge } from '@exodus/checkout-signer'
// Read latest nonce & on-chain ids from the subscription
const sub = await fetch(`https://checkout.exodus.com/subscriptions/${subscriptionId}`, {
headers: { Authorization: `Bearer ${process.env.API_KEY}` },
}).then((r) => r.json())
const signature = signCharge(
sub.onchain_id,
sub.charge_amount,
sub.charge_nonce,
sub.subscription_manager_address,
sub.chain,
process.env.SIGNING_PRIVATE_KEY,
)
await fetch(`https://checkout.exodus.com/subscriptions/${sub.id}/charge`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.API_KEY}`,
'Content-Type': 'application/json',
'X-Signature': signature,
},
body: JSON.stringify({
charge_amount: sub.charge_amount,
charge_nonce: sub.charge_nonce,
}),
})Listen for the subscription.charged webhook to know the on-chain outcome — both succeeded and failed attempts fire it.
Test Your Integration
Use test mode to verify your integration works:
- Use your test API key (
sk_test_...) - Create a test subscription checkout
- Complete the subscribe flow using a testnet wallet
- Verify your webhook receives
subscription_checkout.completed
Test mode transactions use testnet networks, so no real funds are transferred.
Go Live
When you’re ready to accept real payments:
- Switch to live API keys - Replace
sk_test_keys withsk_live_keys from your dashboard - Configure your mainnet settlement address - Ensure your production wallet address is set
- Update webhook endpoints - Ensure your production webhook URL is configured
- Verify your integration - Run through the complete payment flow one more time
Important: Always test thoroughly in test mode before processing live transactions.
Next Steps
Now that you’ve created your first subscription:
- Create checkouts for one-time payments
- Sign subscription actions — charge, cancel, update price
- Configure webhooks for all event types
- Explore the full API Reference for all available endpoints
