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.
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
| Header | Description | Required |
|---|---|---|
| Authorization | Bearer token with your API key | yes |
| Content-Type | application/json | yes |
| X-Signature | Signature from `signCancelSubscription(...)` | yes (unless `cancel_at_period_end: true`) |
Path Parameters
| Name | Type | Description | Required |
|---|---|---|---|
| id | string | The subscription ID (e.g. `sub_abc123def456`). | yes |
Body Parameters
| Name | Type | Description | Required |
|---|---|---|---|
| deadline | number | Unix timestamp (seconds) after which the signature expires. Same value passed to `signCancelSubscription`. | yes (when sending a signature) |
| cancel_at_period_end | boolean | When `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
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
{
"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"
}{
"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
| Status | Code | Description |
|---|---|---|
| 400 | subscription_already_cancelled | Subscription is already cancelled on-chain. |
| 400 | invalid_signature | Signature does not recover to the merchant’s registered signing address. |
| 400 | signature_expired | deadline has elapsed. |
| 400 | nonce_mismatch | Signature was produced against a stale charge_nonce. Re-fetch and re-sign. |
| 404 | not_found | Subscription ID does not exist. |
| 403 | forbidden | Subscription belongs to another merchant. |
Webhook Fired
subscription.cancelled — fires once on the active|cancelling → cancelled transition (when the on-chain SubscriptionCancelled event is indexed).
