BlogMetering

How to cancel a subscription without burning the bridge (or your data)

The user cancels. Do they drop to a free tier, or lose access when the paid month runs out? Those are different products, different SDK calls, and different mistakes when you get them wrong.

Cancellation is a fork, not a delete

The instinct when a user cancels is subtractive: delete the subscription row, zero the counters, clean up. Every part of that is wrong. Deleting the subscription destroys the history you need for churn analysis and the dispute that arrives in 60 days. Zeroing counters erases consumption that already happened. And "cleanup" usually runs from a billing webhook that will, someday, be delivered twice. Cancellation done right is additive - a state transition appended to history - and the only real decision is what the user’s access becomes afterward. There are exactly two good answers.

Pattern one: cancel means downgrade

If your app has a free tier, cancellation is not an ending - it is a plan change to free. One call, the same upsert you used at signup. The subscription stays alive, a plan_changed transition lands in history, and the user keeps using the product under free limits. This is the pattern that preserves the relationship: the user stays in the product, keeps their data and their habits, and your win-back motion is an upgrade nudge instead of a re-acquisition campaign.

// Billing webhook: subscription cancelled -> drop to free
if (event.type === 'subscription.canceled') {
  await vevee.upsertSubscription({
    userId: event.userId,
    planId: 'free', // logs plan_changed; user keeps free-tier access
  });
}

Pattern two: cancel means a scheduled stop

No free tier? Then cancellation means access ends - but almost never immediately, because the user paid through the end of the period. cancelSubscription with endsAt set to the period end gives you the standard SaaS behavior with no scheduler: the user keeps full access until the timestamp passes, and then canUse and reserve start returning no_subscription on their own. Nothing fires at midnight, nothing can fail to fire - the expiry is a property of the subscription, evaluated at request time.

// Paid-only app: access survives until the period the user paid for ends
await vevee.cancelSubscription({
  userId: event.userId,
  endsAt: new Date(event.currentPeriodEndUnix * 1000).toISOString(),
});

// After endsAt passes - no cron involved:
// canUse / reserve -> { allowed: false, reasons: ['no_subscription'] }

What happens to the events of the departed

A detail most metering systems fumble: the user whose access ended may still trigger calls - a stale client, a queued job, an API integration they forgot. Vevee’s track still records those events, marked no_subscription, with zero counter increments and zero cost attribution. Access enforcement and historical record stay separate concerns: the user is blocked where blocking matters, but your analytics never develops a blind spot, and when they reactivate - one upsertSubscription call, logged as reactivated - their full timeline is intact on both sides of the gap.

Pick one pattern per app. Seriously.

The patterns are both correct and mutually exclusive. Mixing them - downgrading some cancels to free while scheduling stops for others, in the same app - produces a subscription history that no analyst can read: was this user churned or free? Did that cohort lapse or downgrade? Every downstream metric forks on a distinction your own data cannot make. The rule is mechanical: free tier exists, cancel is upsert to free; no free tier, cancel is cancelSubscription with endsAt. Decide once, write it in the runbook, and your churn dashboard stays a dashboard instead of a forensic investigation.

See your own numbers in two minutes.Add the SDK and your users, costs and revenue fill in on the next request.
Join waitlist

Keep reading

ShareCopy a link to this post.

Set your first limit in about five minutes.

50k operations free every month. No card required.
Join waitlist