Cancel Subscription
POST/subscriptions/:id/cancelDescription
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
| Header | Description | Required |
|---|---|---|
| Authorization | Bearer token with your API key | yes |
| Content-Type | application/json | yes |
| X-Signature | Signature from `signCancelSubscription(...)` | yes |
Path Parameters
| Name | Type | Description | Required |
|---|---|---|---|
| id | string | The 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
| Name | Type | Description | Required |
|---|---|---|---|
| deadline | number | Unix timestamp (seconds) after which the signature expires. Same value passed to `signCancelSubscription`. | yes |
Producing the Signature
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
{
"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
| Status | Code | Description |
|---|---|---|
| 400 | SignatureExpired | deadline had elapsed by the time the contract validated the signature. |
| 400 | InvalidNonce | Signature was produced against a stale charge_nonce. Re-fetch and re-sign. |
| 400 | InvalidSignature | Contract rejected the signature. |
| 400 | OnChainRevert | Receipt reverted with an unrecognized code. |
| 401 | (no code) | Signature does not recover to your registered signing address (error.type: "authentication_error", API-level preflight). |
| 404 | not_found | Subscription ID does not exist. |
Webhook Fired
subscription.cancelled — fires once on the cancelling → cancelled transition (when the on-chain SubscriptionCancelled event is indexed).
