Skip to Content

Cancel Subscription

POST/subscriptions/:id/cancel

Description

Cancel an active subscription as the merchant. The Exodus API submits the on-chain cancelWithSig transaction; Exodus pays the gas.

This is a signed request — sign with signCancelSubscription from @exodus/checkout-signer, then post the resulting signature in the X-Signature header.

For customer-initiated cancels (the customer pays gas), surface the hosted https://checkout.exodus.com/cancel/:subscription_id URL instead.

Two cancel modes. Pass cancel_at_period_end: true to set the off-chain honor flag (the merchant scheduler skips further charges, contract stays active until the customer or merchant submits a real cancelWithSig later). Pass cancel_at_period_end: false (default) or omit it to submit the cancel on-chain immediately.

Headers

HeaderDescriptionRequired
AuthorizationBearer token with your API keyyes
Content-Typeapplication/jsonyes
X-SignatureSignature from `signCancelSubscription(...)`yes (unless `cancel_at_period_end: true`)

Path Parameters

NameTypeDescriptionRequired
idstringThe subscription ID (e.g. `sub_abc123def456`).yes

Body Parameters

NameTypeDescriptionRequired
deadlinenumberUnix timestamp (seconds) after which the signature expires. Same value passed to `signCancelSubscription`.yes (when sending a signature)
cancel_at_period_endbooleanWhen `true`, set the off-chain `cancel_at_period_end` honor flag and do NOT submit on-chain. The subscription transitions to `cancelling` status. Default: `false`.no

Producing the Signature

cancel-subscription.js
import { signCancelSubscription } from '@exodus/checkout-signer'
 
// Fetch current subscription state.
const sub = await fetch('https://checkout.exodus.com/subscriptions/sub_abc123def456', {
  headers: { Authorization: `Bearer ${process.env.API_KEY}` },
}).then((r) => r.json())
 
const deadline = Math.floor(Date.now() / 1000) + 60 * 60 // 1 hour from now
 
const signature = signCancelSubscription(
  sub.onchain_id,
  sub.charge_nonce,
  deadline,
  sub.subscription_manager_address,
  sub.chain,
  process.env.SIGNING_PRIVATE_KEY,
)
 
const response = await fetch(
  `https://checkout.exodus.com/subscriptions/${sub.id}/cancel`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.API_KEY}`,
      'Content-Type': 'application/json',
      'X-Signature': signature,
    },
    body: JSON.stringify({ deadline }),
  },
)

Cancel at Period End (no signature)

To set the off-chain honor flag without submitting on-chain, send the body without a signature:

await fetch(`https://checkout.exodus.com/subscriptions/${sub.id}/cancel`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ cancel_at_period_end: true }),
})

The subscription transitions to cancelling (off-chain flag set). The contract remains active. The merchant scheduler MUST skip further charge calls. When the customer cancels via the hosted page (or the merchant later submits a real cancelWithSig), the subscription transitions to cancelled.

Response

CANCELLED ON-CHAIN
{
  "object": "subscription",
  "id": "sub_abc123def456",
  "status": "cancelled",
  "subscription_checkout_id": "schk_1234567890abcdef",
  "onchain_id": "0x9f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a",
  "subscriber": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE21",
  "chain": "eip155:1",
  "cancel_at_period_end": false,
  "cancelled_at": "2026-05-19T10:30:00Z",
  "created_at": "2026-02-19T12:02:18Z"
}
CANCEL AT PERIOD END (HONOR FLAG SET)
{
  "object": "subscription",
  "id": "sub_abc123def456",
  "status": "cancelling",
  "subscription_checkout_id": "schk_1234567890abcdef",
  "onchain_id": "0x9f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a",
  "subscriber": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE21",
  "chain": "eip155:1",
  "cancel_at_period_end": true,
  "cancelled_at": null,
  "created_at": "2026-02-19T12:02:18Z"
}

Errors

StatusCodeDescription
400subscription_already_cancelledSubscription is already cancelled on-chain.
400invalid_signatureSignature does not recover to the merchant’s registered signing address.
400signature_expireddeadline has elapsed.
400nonce_mismatchSignature was produced against a stale charge_nonce. Re-fetch and re-sign.
404not_foundSubscription ID does not exist.
403forbiddenSubscription belongs to another merchant.

Webhook Fired

subscription.cancelled — fires once on the active|cancelling → cancelled transition (when the on-chain SubscriptionCancelled event is indexed).

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