vevee.cancelSubscription()DELETE /api/v1/subscriptionssk_live_

End a user's subscription. Once endsAt is reached (immediately if you omit it), canUse and reserve start returning { allowed: false, matched: false, reasons: ['no_subscription'] } - the user is blocked from consuming any metered feature. track still records the attempt with match_status: 'no_subscription' but increments nothing and bills nothing.

!
If your app has a free plan, don't use this. On downgrade just call upsertSubscriptionwith the free plan's id - the user keeps using your app under free-tier limits. Use cancelSubscription only when your product requires a paid subscription and a churned user should be hard-blocked. See the downgrade vs cancel guide.

Signature

cancelSubscription(params: {
  userId: string;
  endsAt?: string;       // ISO 8601. Omit to cancel immediately.
  reason?: string;       // Free-text label, stored on the history row.
}): Promise<CancelSubscriptionResponseData>

Response

interface CancelSubscriptionResponseData {
  subscriptionId: string;   // 'sub_…'
  userId: string;
  planId: string;           // The plan the user was on at cancel time.
  endsAt: string;           // ISO 8601 - when the cancellation takes effect.
}

Examples

Cancel immediately

await vevee.cancelSubscription({
  userId: 'user_abc123',
  reason: 'user_cancel',
});
// canUse / reserve start returning allowed: false right away.

Schedule cancellation at the end of the paid period

Common Stripe pattern - let the user keep using the plan until their current billing period ends, then block. Pass the period end as endsAt:

// app/api/webhooks/stripe/route.ts
if (event.type === 'customer.subscription.updated') {
  const sub = event.data.object;
  if (sub.cancel_at_period_end) {
    await vevee.cancelSubscription({
      userId: sub.metadata.app_user_id,
      endsAt: new Date(sub.current_period_end * 1000).toISOString(),
      reason: 'stripe_period_end_cancel',
    });
  }
}

Reactivate a canceled user

Just call upsertSubscription with the plan you want them on. That clears ends_at on the current-state row and appends a fresh entry to the subscription history.

await vevee.upsertSubscription({
  userId: 'user_abc123',
  planId: 'plan_pro',
});

How it affects metering

MethodBefore endsAtAfter endsAt
canUse()Normal behavior - checks counters against quota.{ allowed: false, matched: false, reasons: ['no_subscription'] }
reserve()Normal behavior - holds quota, returns a reservationId.{ allowed: false, matched: false, reasons: ['no_subscription'] } (no reservation row created)
track()Normal behavior - increments counters.Event row is recorded with match_status: 'no_subscription'; counters and cost are not touched. Lets the dashboard show the failed attempt.
usage()Returns the user's counters.Throws subscription_not_found (HTTP 404). Catch it and show an upsell.

Scheduled cancellation

When endsAt is in the future, the user keeps their current plan and limits until that moment. The subscription row stays active (loadSubscription ignores ends_atwhile it's still in the future). To change your mind before then, call cancelSubscription again with a different endsAt - or call upsertSubscription to clear it entirely.

Subscription history

Each call appends a row to the subscription_events audit log with event_type: 'canceled', the prior planId in from_plan_id, and your reason if provided. That log is append-only - call cancelSubscription as many times as you need, the timeline always reflects reality. See the core concepts page for the full lifecycle model.

Errors

  • not_found (404) - no subscription exists for this userId on this app.
  • already_canceled (409) - the existing ends_at has already elapsed. (Scheduled cancellations with a future ends_at are still updatable.)
  • invalid_request (400) - malformed endsAt or reason over 500 chars.
  • requires_secret_key (401) - you called it with a pk_live_ public key.

Where to go next