Subscriptions
Filter subscriptions and safely schedule or remove cancellation.
Subscriptions are Cipay-owned recurring billing records. Read the latest state before a mutation because revision protects newer changes from being overwritten.
List subscriptions
// Scope merchant tooling to one customer and one lifecycle state.
const active = await cipay.subscriptions.list({
customerId,
status: "active",
pageSize: 20,
});
// Log safe identifiers and state, never customer or payment secrets.
for (const subscription of active.items) {
console.log({
id: subscription.id,
status: subscription.status,
currentPeriodEndsAt: subscription.currentPeriodEndsAt,
});
}
Buyer-facing lists should use createCipayBuyerHandler. It resolves the customer from the verified application session and checks ownership.
Retrieve the latest state
// Retrieve immediately before showing or changing subscription state.
const subscription = await cipay.subscriptions.retrieve(subscriptionId);
console.log({
id: subscription.id,
status: subscription.status,
revision: subscription.revision,
scheduledCancellationAt: subscription.scheduledCancellationAt,
});
Schedule cancellation
First retrieve the subscription as shown above. Then send that exact revision with the cancellation request.
import { createIdempotencyKey } from "@cipay/client-sdk/api";
const updated = await cipay.subscriptions.scheduleCancellation(
subscription.id,
"Customer requested cancellation",
{
revision: subscription.revision,
// A transport retry for this same click must reuse the same key.
idempotencyKey: createIdempotencyKey("cancel"),
},
);
console.log({
id: updated.id,
status: updated.status,
revision: updated.revision,
scheduledCancellationAt: updated.scheduledCancellationAt,
});
Cancellation reason
reason is trimmed free text from 1 to 500 characters. Use a short merchant-friendly explanation.
| Situation | Good reason | Avoid |
|---|---|---|
| Buyer asked to stop | Customer requested cancellation |
A full support transcript |
| Duplicate purchase | Duplicate subscription |
Customer email or phone |
| Product no longer needed | Customer no longer needs service |
Internal account notes |
| Support correction | Created in error by support |
API keys, tokens, card data, or secrets |
Cipay rejects sensitive-looking values. Keep detailed support and audit notes in your own application database.
Why send revision?
revision is the subscription version. Every successful change increments it.
Read
3.Someone else changes it
4.Your old action stops
3 returns a stale-revision error instead of overwriting revision 4.const reason = "Customer requested cancellation";
try {
// This may fail if another action changed the subscription first.
await cipay.subscriptions.scheduleCancellation(subscription.id, reason, {
revision: subscription.revision,
idempotencyKey: createIdempotencyKey("cancel"),
});
} catch (error) {
// Refresh the UI and ask the user to confirm against the current state.
const latest = await cipay.subscriptions.retrieve(subscription.id);
console.log({ status: latest.status, revision: latest.revision });
}
Do not automatically replay an old user decision after a stale-revision error.
Remove scheduled cancellation
Retrieve the subscription again immediately before this action. The returned revision may have changed since the cancellation was scheduled.
const current = await cipay.subscriptions.retrieve(subscriptionId);
console.info({
revision: current.revision,
scheduledCancellationAt: current.scheduledCancellationAt,
});
Pass that revision to removeScheduledCancellation. A successful response is the new authoritative subscription state.
import { createIdempotencyKey } from "@cipay/client-sdk/api";
const updated = await cipay.subscriptions.removeScheduledCancellation(
current.id,
{
revision: current.revision,
idempotencyKey: createIdempotencyKey("keep-subscription"),
},
);
console.info({
id: updated.id,
status: updated.status,
revision: updated.revision,
scheduledCancellationAt: updated.scheduledCancellationAt,
});
{
"id": "subscription_123",
"customerId": "customer_123",
"status": "active",
"revision": 5,
"scheduledCancellationAt": null,
"currentPeriodEndsAt": "2026-10-09T00:00:00.000Z",
"planSnapshot": {
"name": "Monthly coffee",
"amountHalalas": 4500,
"currency": "SAR",
"cadence": { "intervalUnit": "month", "intervalCount": 1 }
}
}
Each new button click gets a new idempotency key. A network retry of that same click reuses its original key.