Skip to Content
CheckoutQuickstart

Quickstart

Start accepting recurring stablecoin payments in just a few minutes.

Prerequisites

Before you begin, you’ll need:

  1. An Exodus Business account - Contact [email protected] to create your account and complete the onboarding process
  2. API credentials - Once your account is approved, you’ll find your API keys in your dashboard settings
  3. Settlement address - Configure the wallet address where you’ll receive stablecoin payments
  4. Webhook secret - Configure your webhook endpoint URL in the dashboard to receive your webhook signing secret
  5. 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-signer
setup.js
import { 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 manager

Store 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.

direct-payment.js
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.

server.js
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:

client.js
// After receiving the hosted URL from your server
window.location.href = hostedUrl

The customer will:

  1. Connect their wallet (Exodus, Grateful, MetaMask, Phantom, etc.)
  2. Pick the chain to subscribe on (out of supported_chains)
  3. Sign the on-chain subscribeAndCharge — first charge clears in the same transaction
  4. Be redirected to your success_url once confirmed

Handle Webhooks

Set up a webhook endpoint to receive subscription events:

webhooks.js
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:

charge.js
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:

  1. Use your test API key (sk_test_...)
  2. Create a test subscription checkout
  3. Complete the subscribe flow using a testnet wallet
  4. 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:

  1. Switch to live API keys - Replace sk_test_ keys with sk_live_ keys from your dashboard
  2. Configure your mainnet settlement address - Ensure your production wallet address is set
  3. Update webhook endpoints - Ensure your production webhook URL is configured
  4. 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:

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