Contents

Cancel subscription

Stop a subscription renewing: the paid period runs out, nothing more is billed, and nothing is refunded.

Source src/operations/cancelSubscription.ts#L30cancelSubscriptionsrc/operations/cancelSubscription.ts#L70assertMayCancelsrc/operations/guards.ts#L166lifecycleMarker

API cancelSubscription

cancelSubscription marks an active Subscription CANCELED, so the background worker’s renewal sweep stops billing it. It moves no money.

You submit it with just the subscription id:

const outcome = await economy.submit({
  kind: 'cancelSubscription',
  idempotencyKey: 'idem_1',
  actor: { kind: 'user', userId: 'usr_a' },
  subscriptionId: 'sub_abc',
});
// → { status: "committed", transaction: { id: "txn_…", … } }
curl -s https://economy.example/submit \
  -H 'content-type: application/json' \
  -d '{
    "kind": "cancelSubscription",
    "idempotencyKey": "idem_1",
    "actor": { "kind": "user", "userId": "usr_a" },
    "subscriptionId": "sub_abc"
  }'

Canceling forfeits the rest of the period already paid for. There is no refund, so there is nothing to post to the ledger.

Parameters

The payload fields, beyond the kind tag, are:

FieldTypeDefaultDescription
idempotencyKeystring(required)A retried submit with the same key runs at most once. See idempotency.
actorPrincipal(required)Who is asking. A user actor may cancel only their own subscription; a system or operator actor may cancel anyone’s.
subscriptionIdstring(required)The subscription to cancel. Non-blank.

Returns

cancelSubscription resolves to an Outcome.

  • committed: the subscription is CANCELED. The placeholder Transaction carries empty legs — no money moves.
  • duplicate: a repeat of the same idempotencyKey.
  • rejected: the subscription is missing or already CANCELED, with UNKNOWN_SUBSCRIPTION. See Reason codes.

Postings

None. A cancel is a status change only, so the placeholder transaction posts no double-entry legs and advances no account’s hash chain.

Authorization

A user actor may cancel only their own subscription. The handler loads the record and compares its owner to the actor’s userId; a mismatch throws AUTH.UNAUTHORIZED.

A system or operator actor may cancel anyone’s subscription. cancelSubscription is not a privileged-only operation.

Reason codes

cancelSubscription returns these reason codes as a rejected outcome, a normal “no”, not a thrown fault:

CodeWhen
UNKNOWN_SUBSCRIPTIONNo subscription matches the id, or it is already CANCELED.
ECONOMY_PAUSEDA maintenance window is in effect and the actor is a user. The decline carries resumesAt.

A malformed request throws instead:

CodeWhen
OP.MALFORMEDA blank or whitespace-only subscriptionId. Malformed client input, caught before the store is touched.
AUTH.UNAUTHORIZEDA user actor cancels a subscription they do not own.

Preconditions and invariants

The ownership check runs only after the subscription is confirmed to exist and be cancelable. So probing a missing or already-canceled id returns the same UNKNOWN_SUBSCRIPTION answer regardless of caller, and never reveals whether the id exists.

Canceling is final for that record: a CANCELED subscription stays canceled, and a second cancel of the same id returns UNKNOWN_SUBSCRIPTION rather than committing again. To bill the same SKU and seller again, the user runs subscribe to open a fresh subscription.

See also