Skip to content
Cipay
Esc
navigateopen⌘Jpreview
On this page

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

Retrieve revision 3.

Someone else changes it

Another action succeeds and creates revision 4.

Your old action stops

Sending revision 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.