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).
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
Install the SDK — you use it to sign every fund-moving request:
npm install @exodus/checkout-signerThese examples use the CheckoutSigner client from @exodus/checkout-signer 0.5.0 or later,
which takes the single sgk_… Signing Key. Earlier releases took a raw mnemonic.
Get Your Signing Key
Generate a Signing Key in the Exodus dashboard. Under Settings → Signing key, generate a single sgk_… secret, shown once, that you treat like an API key. The dashboard registers your signer addresses in the same step, so your account is ready to sign with nothing to send separately, and it handles rotation. Store the key as SIGNING_KEY.
Signing a Request
You sign every fund-moving request with the CheckoutSigner client (below). Where the signature goes depends on the request:
- Checkout creation (direct payments): the signature travels in the request body, inside the
signaturesarray. A checkout can be payable on more than one chain, so it carries one entry per chain family. Each entry is{ chain_family, on_chain_id, signature, deadline }. These per-chain deploy signatures are data stored on the payment, so they belong in the body. - Every other signed action (charge, cancel, capture, refund, rescue, settlement change): a single signature authorizes one operation, so it travels in the
X-Signatureheader (the action parameters go in the body).
In short: the signatures that describe the resource go in the body; the one signature that authorizes a request goes in the header.
On EVM chains, every merchant signature (payments and subscriptions) is EIP-712 typed data, so it renders as structured, human-readable fields in the signer’s wallet rather than as an opaque hash. Solana actions are signed as ed25519 messages.
Construct CheckoutSigner once (with no arguments it reads your SIGNING_KEY from the environment), then call the method for your action with the API response object. It derives the right per-chain key, fills in the signed fields, defaults the deadline, and returns { signature, body }.
import { CheckoutSigner } from '@exodus/checkout-signer';
// Reads SIGNING_KEY from the environment
const signer = new CheckoutSigner();Subscriptions
Pass the GET /subscriptions/:id object straight in — subscriptions sign on any supported chain (the client reads the chain off the object).
// Charge — you choose the amount per charge (the plan price here); the body carries the same amount
const { signature } = signer.signCharge(subscription, { amount: BigInt(subscription.price) });
// Send signature in the X-Signature header and { amount: subscription.price } as the body
// to POST /subscriptions/:id/charge
// Cancel (immediate, on-chain, terminal) — body carries the deadline the signature commits to
const cancel = signer.signCancelSubscription(subscription);
// Send cancel.signature in X-Signature and cancel.body to POST /subscriptions/:id/cancelTwo-step payments
Pass the GET /payments/:id object (or the payment.escrow_confirmed webhook payload).
// Capture — amount comes from the payment's token_amount
const capture = signer.signCapture(payment);
// Send capture.signature in X-Signature and capture.body to POST /payments/:id/capture
// Refund to the customer — destination is bound into the signature
const refund = signer.signRefund(payment, { destination }); // e.g. destination = payer_addresses[0]
// Send refund.signature in X-Signature and refund.body to POST /payments/:id/refundDirect payment (checkout creation)
const signatures = signer.createDirectPayment({ factory_addresses: { evm: factoryAddress } });
// signatures = { evm: { on_chain_id, signature, deadline }, ... }
// Send one entry per family as { chain_family, on_chain_id, signature, deadline } in the `signatures` array of POST /checkoutsRescue and settlement change
// Rescue stuck funds — pass the payment object plus the token to recover and its receiver
const rescue = signer.signRescue(payment, { token, receiver });
// Send rescue.signature in X-Signature and rescue.body to POST /payments/:id/rescue
// Change the settlement address — read contract_address + nonce from GET /payments/settlement
const settlement = signer.signSettlementChange({ contract_address, new_address, nonce, chain });
// Send settlement.signature in X-Signature and settlement.body to PUT /payments/settlementRaising the cap is subscriber-authorized, not merchant-signed: the subscriber calls updateCap on-chain. A merchant key can only charge within the subscriber’s existing cap and budget, or cancel.
Security Best Practices
- 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.
- Separate it from your API key. Your signing key and API key serve different purposes, so compromising one does not compromise the other.
- 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.
