Skip to Content
CheckoutAPI ReferenceSigned Requests

Signed Requests

Some API requests require a cryptographic signature to authorize the action. This applies to any request that creates or moves funds. That includes creating direct payments, capturing, and refunding payments, as well as all merchant-driven subscription actions (charge, cancel, update price).

Why Are Some Requests Signed?

Your API key authenticates who you are, but it is not enough to move funds. Direct payments, captures, and refunds transfer stablecoins between wallets, so they require proof that you authorized the action.

This authorization is done with a signing key: a separate cryptographic key-pair that you control. The SDK provides purpose-built signing functions for each action; you call the appropriate function with your signing key and include the resulting signature with the request. The API verifies the signature before executing the action.

🔐

Your API key alone cannot move funds. All fund-moving operations require a valid signature from your signing key.

Setting Up Your Signing Key

1. Generate a Key-Pair

Install the SDK and generate your key:

npm install @exodus/checkout-signer
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 Exodus
console.log('Solana signer address:', solana.address) // send to Exodus

This generates a BIP-39 mnemonic and derives one signer key per supported chain family. Store the mnemonic securely (it is the single secret behind every chain), and register evm.address and solana.address with Exodus. You can also generate the mnemonic from your Exodus dashboard.

For an EVM-only integration you can skip the mnemonic and generate a single key with generateSigningKey('evm'). Pass its privateKey wherever signCapture and signRefund below accept a mnemonic. Checkout creation is the one exception: signDirectPaymentMultiChain takes a mnemonic, so a private-key-only merchant signs the deploy with signDirectPayment from @exodus/checkout-signer/evm instead.

2. Register Your Signer Address

Send your signer address to your account manager. Once received, we will configure your account for signed payments.

Signing a Request

The SDK provides purpose-built functions for each action. Where the signature goes depends on the request:

  • Checkout creation (direct payments): the signature travels in the request body, inside the signatures object. A checkout can be payable on more than one chain, so it carries one signature per chain family, keyed by family. These per-chain deploy signatures are data stored on the payment, so they belong in the body.
  • Every other signed action (capture, refund, rescue, settlement change): a single signature authorizes one operation, so it travels in the X-Signature header (the action parameters go in the body).

In short: many signatures that describe the resource go in the body; the one signature that authorizes a request goes in the header.

Available Signing Functions

signCapture and signRefund are chain-agnostic: import them from the package root, pass the CAIP-2 chain, and the SDK routes to the correct signer. Give it your mnemonic (the per-chain key is derived for you) or a per-chain privateKey. They sign over values Exodus reports on the payment webhook: on_chain_id, deposit_address, and detected_chain (the CAIP-2 chain the customer paid on, e.g. eip155:1).

Capture (collect an authorized two-step payment):

import { signCapture } from '@exodus/checkout-signer'
 
const signature = signCapture({
  chain, // detected_chain (CAIP-2), e.g. 'eip155:1' or 'solana:...'
  paymentId: onChainId, // the payment's on_chain_id
  depositAddress, // deposit_address from the webhook
  mnemonic: process.env.SIGNING_MNEMONIC,
})
// Send signature in X-Signature header to POST /payments/:id/capture

Refund (return funds to the customer; the destination is part of the signature):

import { signRefund } from '@exodus/checkout-signer'
 
const deadline = Math.floor(Date.now() / 1000) + 3600 // signature expiry (unix seconds)
const signature = signRefund({
  chain,
  paymentId: onChainId,
  depositAddress,
  destination, // customer address, e.g. payer_addresses[0]
  deadline,
  mnemonic: process.env.SIGNING_MNEMONIC,
})
// Send the signature in X-Signature, and the same destination + deadline in the body, to POST /payments/:id/refund

Direct payment (create a new payment, one signature per chain you accept):

import { signDirectPaymentMultiChain } from '@exodus/checkout-signer'
 
const signatures = signDirectPaymentMultiChain(process.env.SIGNING_MNEMONIC, {
  factoryAddresses: { evm: factoryAddress },
})
// signatures = { evm: { onChainId, signature }, solana: { onChainId, signature } }
// Send each entry as { on_chain_id, signature } in the `signatures` body of POST /checkouts

Rescue and settlement change have no chain-agnostic root helper yet, so import them from @exodus/checkout-signer/evm (or @exodus/checkout-signer/solana) and pass a per-chain privateKey. Derive it from your mnemonic with signingKeysFromMnemonic(mnemonic).evm.privateKey.

import { signRescue, signSettlementChange } from '@exodus/checkout-signer/evm'
 
// Rescue excess deposits from a direct payment (deadline defaults to ~24h and is returned)
const { signature, deadline } = signRescue({
  paymentId: onChainId,
  paymentContractAddress: depositAddress, // the payment contract holding the funds
  token,
  receiver,
  chain,
  privateKey,
})
// Send signature in X-Signature, deadline in the body, to POST /payments/:id/rescue
 
// Change the settlement address for a chain (nonce-based)
const settlementSignature = signSettlementChange(
  factoryAddress,
  newAddress,
  nonce,
  chainId,
  privateKey,
)
// Send signature in X-Signature header to PUT /payments/settlement

Subscription Actions

Subscription actions are also signed. Each action is bound to the subscription’s on-chain id, the SubscriptionManager contract address, and the CAIP-2 chain the subscription lives on — all three values are returned by GET /subscriptions/:id. Every signer uses an action-specific nonce so replays cannot move funds twice.

All subscription signers accept the merchant-facing CAIP-2 chain identifier (e.g. eip155:1, eip155:137) as the chain argument. The SDK extracts the numeric chainId internally.

Like rescue and settlement, these signers are EVM-only and take a per-chain privateKey. A mnemonic-onboarded merchant derives it with signingKeysFromMnemonic(mnemonic).evm.privateKey.

Charge a cycle (advances the period clock):

import { signCharge } from '@exodus/checkout-signer/evm'
 
const signature = signCharge(
  onchainId,
  amount,
  chargeNonce,
  subscriptionManagerAddress,
  chain,
  privateKey,
)
// Send amount in the body and the signature in the X-Signature header to POST /subscriptions/:id/charge

amount is the per-cycle settlement-token amount. Fetch it first from POST /subscriptions/:id/charge-quote, then sign exactly that value. Token-denominated plans return the fixed price, while FIAT-denominated plans return a short-lived exact-out quote.

Cancel a subscription (immediate, on-chain, and terminal):

import { signCancelSubscription } from '@exodus/checkout-signer/evm'
 
const deadline = Math.floor(Date.now() / 1000) + 600 // 10 minutes in the future
const signature = signCancelSubscription(
  onchainId,
  chargeNonce,
  deadline,
  subscriptionManagerAddress,
  chain,
  privateKey,
)
// Send signature in X-Signature header AND deadline in body to POST /subscriptions/:id/cancel

deadline is a Unix timestamp after which the signature can no longer be replayed. Send the same value in the request body.

A cancel_at_period_end request (honor-flag-only) does NOT touch the chain and therefore does NOT need a signature. Pass { "cancel_at_period_end": true } to POST /subscriptions/:id/cancel with no X-Signature header. See Cancel a Subscription.

Plan upgrades/downgrades (raising the cap, with optional proration) are subscriber-authorized, not merchant-signed: the subscriber consents on the hosted page (modifySubscription). There is no merchant signing helper for them. A merchant key can only charge within the subscriber’s existing cap or cancel.

Security Best Practices

  1. Keep your private key secret. Store it in a secure environment such as environment variables or a secrets manager. Never expose it in client-side code or version control.
  2. Separate it from your API key. Your signing key and API key serve different purposes, so compromising one does not compromise the other.
  3. Rotate it if compromised. If you suspect your signing private key has been compromised, contact your account manager immediately to register a new signer address.

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