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:
DELETE /v1/payment/subscriptions/:id?immediate=true— immediate cancel.- A deferred cancel hits its scheduled
cancelAtPeriodEndand the daily cron runs. - 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. ReadcanceledAt— if it equalscurrentPeriodEnd, that's the deferred case; if earlier, it was immediate. Or just compareevent.createdAttocurrentPeriodEnd. - 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
subscription.past_due— what precedes a dunning-exhausted cancel.subscription.created— the inverse.