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

Install the SDK — you use it to sign every fund-moving request:

npm install @exodus/checkout-signer
📦

These 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.

1. Get Your Signing Key

The recommended way is the Exodus dashboard. Under Settings → Signing key, generate a Signing Key: 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.

To generate one programmatically instead — for example, to manage rotation from your own tooling — use CheckoutSigner.generate():

import { CheckoutSigner } from '@exodus/checkout-signer';
 
const { signingKey, addresses } = CheckoutSigner.generate();
 
console.log('Signing Key:', signingKey); // store securely as SIGNING_KEY (shown once)
console.log('EVM signer address:', addresses.evm);
console.log('Solana signer address:', addresses.solana);

generate() mints one Signing Key that derives a signer for every chain family. The underlying mnemonic and private keys never leave the SDK; the sgk_… string is the only secret you store.

2. Register Your Signer Address

If you generated your key in the dashboard, your signer addresses are already registered — skip this step. If you generated or rotated the key yourself, send addresses.evm and addresses.solana to your account manager so we can configure (and redeploy) your account for signed payments.

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 signatures array. 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 (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: 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 a cycle — no request body
const { signature } = signer.signCharge(subscription);
// Send signature in the X-Signature header 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/cancel

Two-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/refund

Direct 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 /checkouts

Rescue 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/settlement

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.

Raising the cap (with optional proration) is subscriber-authorized, not merchant-signed: the subscriber consents on the hosted page (modifySubscription). 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