subscription.canceled

Fires when a subscription terminates — either immediately (admin override, dunning exhaustion) or at the end of the current cycle (DELETE /v1/payment/subscriptions/:id without ?immediate=true). After this event, no further charges fire and the subscription is status: canceled permanently.

When it fires

Three code paths emit it:

  1. DELETE /v1/payment/subscriptions/:id?immediate=true — immediate cancel.
  2. A deferred cancel hits its scheduled cancelAtPeriodEnd and the daily cron runs.
  3. Dunning exhausted (all retries failed) and the workspace's policy is "cancel on unpaid".

The event is single-shot per subscription. Re-deleting an already-canceled subscription is a no-op.

Payload

{
  "id": "evt_01HX...",
  "type": "subscription.canceled",
  "createdAt": "2026-05-13T10:42:00Z",
  "accountId": "acc_01HX...",
  "data": {
    "id": "sub_01HX...",
    "accountId": "acc_01HX...",
    "customerId": "cust_01HX...",
    "planId": "plan_01HX...",
    "status": "canceled",
    "currentPeriodStart": "2026-05-01T00:00:00Z",
    "currentPeriodEnd": "2026-05-31T23:59:59Z",
    "canceledAt": "2026-05-13T10:42:00Z",
    "metadata": {},
    "createdAt": "2026-04-15T10:00:00Z",
    "updatedAt": "2026-05-13T10:42:00Z"
  }
}

canceledAt is when the cancel actually executed. For deferred cancels, that's the period-end moment. For immediate cancels, it's "now".

Handler examples

// Node
if (event.type === 'subscription.canceled') {
  const s = event.data;
  await access.revoke(s.customerId, s.planId);
  await emails.sendCancellationConfirmation(s.customerId);
  await analytics.track('Subscription Canceled', { planId: s.planId, customerId: s.customerId });
}
# Python
if event["type"] == "subscription.canceled":
    s = event["data"]
    access.revoke(s["customerId"], s["planId"])
    emails.send_cancellation_confirmation(s["customerId"])
// Go
if event.Type == "subscription.canceled" {
    var s storlaunch.Subscription
    _ = json.Unmarshal(event.Data, &s)
    access.Revoke(ctx, s.CustomerID, s.PlanID)
}

What to do

  • Revoke or downgrade the entitlement.
  • Send a cancellation-confirmation email (separate from Storlaunch's receipt-side comms).
  • Trigger your churn-analytics flow.
  • If the cancel came from dunning, consider a "we'd love to keep you" recovery email.

Common pitfalls

  • Revoking too early on deferred cancel. If the buyer asked to cancel at period-end, you typically want to keep access until currentPeriodEnd. Read canceledAt — if it equals currentPeriodEnd, that's the deferred case; if earlier, it was immediate. Or just compare event.createdAt to currentPeriodEnd.
  • Ignoring the dunning case. If metadata.cancelReason === 'dunning_exhausted', the cancel is involuntary; tone of the email matters.
  • Not deduping. The outbox can re-fire.

Related events

Next