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. The signer binds the current charge_nonce and a deadline into the digest; the API reads the nonce from its indexed on-chain state, and the request body carries the deadline so the on-chain contract can enforce the same expiry the signer used.

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

Headers

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

Path Parameters

NameTypeDescriptionRequired
idstringThe on-chain subscription ID (bytes32 hex, e.g. `0x9f3a...`). Obtain it from `GET /subscriptions`, the `subscription.created` webhook, or the `subscription_checkout.completed` webhook.yes

Body Parameters

NameTypeDescriptionRequired
deadlinenumberUnix timestamp (seconds) after which the signature expires. Same value passed to `signCancelSubscription`.yes

Producing the Signature

cancel-subscription.js
import { signCancelSubscription } from '@exodus/checkout-signer/evm'
import { signingKeysFromMnemonic } from '@exodus/checkout-signer'
 
const sub = await fetch('https://checkout-api.exodus-int.com/subscriptions/0x9f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a', {
  headers: { Authorization: `Bearer ${process.env.API_KEY}` },
}).then((r) => r.json())
 
const { privateKey } = signingKeysFromMnemonic(process.env.SIGNING_MNEMONIC).evm
const deadline = Math.floor(Date.now() / 1000) + 60 * 60 // 1 hour from now
 
const signature = signCancelSubscription(
  sub.id,
  sub.charge_nonce,
  deadline,
  sub.subscription_manager_address,
  sub.chain,
  privateKey,
)
 
const response = await fetch(
  `https://checkout-api.exodus-int.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 }),
  },
)

Response

CANCEL SUBMITTED
{
  "subscription_id": "0x9f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a",
  "status": "cancelling",
  "tx_hash": "0xabc..."
}

The subscription transitions to cancelling as soon as the cancel transaction is confirmed on-chain. The indexer transitions it to cancelled once the SubscriptionCancelled event is ingested.

If the subscription is already cancelled or cancelling, the call is a no-op and returns 200 with tx_hash: null and the existing status.

Errors

StatusCodeDescription
400SignatureExpireddeadline had elapsed by the time the contract validated the signature.
400InvalidNonceSignature was produced against a stale charge_nonce. Re-fetch and re-sign.
400InvalidSignatureContract rejected the signature.
400OnChainRevertReceipt reverted with an unrecognized code.
401(no code)Signature does not recover to your registered signing address (error.type: "authentication_error", API-level preflight).
404not_foundSubscription ID does not exist.

Webhook Fired

subscription.cancelled — fires once on the 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