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.ts
import { generateMnemonicSigningKeys } from '@exodus/checkout-signer'
 
const { mnemonic, evm, solana } = generateMnemonicSigningKeys()
 
console.log('Mnemonic:', mnemonic) // store securely as SIGNING_MNEMONIC
console.log('EVM signer address:', evm.address) // send to your account manager
console.log('Solana signer address:', solana.address) // send to your account manager

Store the mnemonic in your environment variables, and send the evm.address and solana.address to your account manager. You can also generate the mnemonic from your Exodus dashboard.

Create a Direct Payment

This is the simplest payment flow. Funds go directly to your settlement address when the customer pays.

direct-payment.ts
import { signDirectPaymentMultiChain } from '@exodus/checkout-signer'
 
// Your PaymentFactory address, from GET /settings
const factoryAddress = '0xfaceb00cfaceb00cfaceb00cfaceb00cfaceb00c'
const signatures = signDirectPaymentMultiChain(process.env.SIGNING_MNEMONIC, {
  factoryAddresses: { evm: factoryAddress },
})
 
const response = await fetch('https://checkout-api.exodus-int.com/checkouts', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer sk_test_xxxxxxxxxxxxxxxx',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    amount: 2999,
    currency: 'USD',
    payment_method: 'direct',
    signatures: {
      evm: {
        on_chain_id: signatures.evm.onChainId,
        signature: signatures.evm.signature,
      },
    },
    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-api.exodus-int.com/subscription-checkouts', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer sk_test_xxxxxxxxxxxxxxxx',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    token_symbol: 'USDC', // settlement token the subscriber pays in
    price: '9990000', // $9.99 USDC (6 decimals); omit price_currency = token-denominated
    recommended_cap: '120000000', // suggested cap (the subscriber sets the on-chain cap when signing)
    period_duration: 2592000, // 30 days in seconds
    supported_chains: ['eip155:1', 'eip155:137'], // CAIP-2 ids the subscriber picks from 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-int.com/subscribe/schk_...
🪙

cap_amount is the on-chain ceiling, set by the subscriber. They authorize the contract for any amount up to cap_amount, so each cycle charge can vary (e.g. a FIAT-priced plan re-rated to USDC) as long as it stays under the cap. Pass a generous recommended_cap up front to seed it; raising the cap later is a subscriber-authorized action on the hosted page.

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. The 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 {
        // On a 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/evm'
import { signingKeysFromMnemonic } from '@exodus/checkout-signer'
 
const headers = { Authorization: `Bearer ${process.env.API_KEY}` }
const { privateKey } = signingKeysFromMnemonic(process.env.SIGNING_MNEMONIC).evm
 
// Read the latest nonce & on-chain ids from the subscription
const sub = await fetch(`https://checkout-api.exodus-int.com/subscriptions/${subscriptionId}`, {
  headers,
}).then((r) => r.json())
 
// Quote the per-cycle amount (fixed price for token plans, a live FX quote for FIAT plans)
const quote = await fetch(`https://checkout-api.exodus-int.com/subscriptions/${sub.id}/charge-quote`, {
  method: 'POST',
  headers,
}).then((r) => r.json())
 
const signature = signCharge(
  sub.id,
  quote.amount,
  sub.charge_nonce,
  sub.subscription_manager_address,
  sub.chain,
  privateKey,
)
 
await fetch(`https://checkout-api.exodus-int.com/subscriptions/${sub.id}/charge`, {
  method: 'POST',
  headers: { ...headers, 'Content-Type': 'application/json', 'X-Signature': signature },
  body: JSON.stringify({ amount: quote.amount }),
})

Listen for the subscription.charged webhook to learn 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