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-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 ExodusThis 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 /checkoutsCapture (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/captureRefund (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/refundRescue (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/rescueSettlement 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/settlementSubscription 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/chargeCharge 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-adhocchargeNonce 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/canceldeadline 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-amountchargeAmountUpdateNonce is a separate counter from chargeNonce — it increments only on price updates. Read both from GET /subscriptions/:id before signing.
Security Best Practices
- 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.
- Separate from API key — your signing key and API key serve different purposes. Compromising one does not compromise the other.
- Rotate if compromised — if you suspect your signing private key has been compromised, contact your account manager immediately to register a new signer address.
