Skip to Content
CheckoutAPI ReferenceSigned Requests

Signed Requests

Some API requests require a cryptographic signature to authorize the action. This applies to requests that create or move funds — 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 — the merchant — explicitly 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 private key and include the resulting signature in the X-Signature header. 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 { 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 Exodus

This gives you a privateKey (32-byte hex string — store securely) and an address (20-byte hex string — send to Exodus).

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. You sign with your private key and include the signature in the X-Signature header.

Available Signing Functions

Direct payment (creates a new payment with a unique on-chain ID):

import { signDirectPayment } from '@exodus/checkout-signer'
 
const { onChainId, signature } = signDirectPayment(
  process.env.SIGNING_PRIVATE_KEY,
)
// Send onChainId (as `on_chain_id`) and signature to POST /checkouts

Capture (collect an authorized two-step payment):

import { signCapture } from '@exodus/checkout-signer'
 
const signature = signCapture(onChainId, process.env.SIGNING_PRIVATE_KEY)
// Send signature in X-Signature header to POST /payments/:id/capture

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

import { signRefund } from '@exodus/checkout-signer'
 
const signature = signRefund(
  onChainId,
  destination,
  process.env.SIGNING_PRIVATE_KEY,
)
// Send signature in X-Signature header to POST /payments/:id/refund

Rescue (recover excess deposits from a direct payment):

import { signRescue } from '@exodus/checkout-signer'
 
const { signature, deadline } = signRescue(
  onChainId,
  token,
  receiver,
  process.env.SIGNING_PRIVATE_KEY,
)
// Send signature in X-Signature header, deadline in request body, to POST /payments/:id/rescue

Settlement address change (per-chain, nonce-based):

import { signSettlementChange } from '@exodus/checkout-signer'
 
const signature = signSettlementChange(
  factoryAddress,
  newAddress,
  nonce,
  chainId,
  process.env.SIGNING_PRIVATE_KEY,
)
// 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.

Charge a cycle (advances the period clock — replaces stale nonces):

import { signCharge } from '@exodus/checkout-signer'
 
const signature = signCharge(
  onchainId,
  chargeAmount,
  chargeNonce,
  subscriptionManagerAddress,
  chain,
  process.env.SIGNING_PRIVATE_KEY,
)
// Send signature in X-Signature header to POST /subscriptions/:id/charge

Charge ad-hoc (overage / metered — does NOT advance the period clock):

import { signChargeAdHoc } from '@exodus/checkout-signer'
 
const signature = signChargeAdHoc(
  onchainId,
  amount,
  chargeNonce,
  subscriptionManagerAddress,
  chain,
  process.env.SIGNING_PRIVATE_KEY,
)
// Send signature in X-Signature header to POST /subscriptions/:id/charge-adhoc

chargeNonce is shared with signCharge — both cycle and ad-hoc draws increment the same monotonic per-subscription counter.

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

import { signCancelSubscription } from '@exodus/checkout-signer'
 
const deadline = Math.floor(Date.now() / 1000) + 600 // 10 minutes in the future
const signature = signCancelSubscription(
  onchainId,
  chargeNonce,
  deadline,
  subscriptionManagerAddress,
  chain,
  process.env.SIGNING_PRIVATE_KEY,
)
// 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.

Update charge amount (raise or lower the recurring price, bounded by cap_amount):

import { signUpdateChargeAmount } from '@exodus/checkout-signer'
 
const signature = signUpdateChargeAmount(
  onchainId,
  newAmount,
  chargeAmountUpdateNonce,
  subscriptionManagerAddress,
  chain,
  process.env.SIGNING_PRIVATE_KEY,
)
// Send signature in X-Signature header to POST /subscriptions/:id/update-charge-amount

chargeAmountUpdateNonce is a separate counter from chargeNonce — it increments only on price updates. Read both from GET /subscriptions/:id before signing.

Security Best Practices

  1. Keep your private key secret — store it in a secure environment (e.g., environment variables, secrets manager). Never expose it in client-side code or version control.
  2. Separate from API key — your signing key and API key serve different purposes. Compromising one does not compromise the other.
  3. Rotate 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