Skip to content
revenue-sdk
Esc
navigateopen⌘Jpreview
On this page

Status mapping

The unified subscription status table with per-provider mappings, how cancelAtPeriodEnd is detected, the date fields, and checkout status mapping.

A complete reference of how each provider’s subscription status maps onto the unified SubscriptionStatus union, plus how a scheduled cancellation is detected. For the conceptual overview see Subscription lifecycle.

The unified union

type SubscriptionStatus =
  | 'incomplete'
  | 'trialing'
  | 'active'
  | 'past_due'
  | 'unpaid'
  | 'paused'
  | 'canceled';

canceled is terminal. A scheduled “cancel at period end” keeps the status unchanged and sets cancelAtPeriodEnd: true plus endsAt.

Status mapping

Unified Polar Lemon Squeezy Stripe Paddle Dodo Payments
incomplete incomplete incomplete pending
trialing trialing on_trial trialing trialing
active active active, cancelled¹ active active active
past_due past_due past_due past_due past_due on_hold
unpaid unpaid unpaid unpaid
paused paused paused paused, pause_collection² paused
canceled canceled, incomplete_expired expired canceled, incomplete_expired canceled cancelled, failed, expired
¹ Lemon Squeezy’s cancelled means “grace period until ends_at, still resumable” — it maps to active with cancelAtPeriodEnd: true. The terminal status is expired. ² Stripe has two unrelated pause mechanisms. pause_collection leaves Stripe’s own status alone — per Stripe’s docs, “the subscription status will be unchanged and will not be updated to paused” — while Stripe’s raw paused status only occurs when a trial ends without a payment method. Both map to the unified paused, except on a terminally canceled subscription: canceled is terminal and outranks a leftover pause_collection.

A dash means the provider has no equivalent state. Any unrecognized provider status falls back to active, so a newly introduced status can’t silently revoke a paying customer’s access; inspect raw if you need the exact provider value.

cancelAtPeriodEnd detection

Provider Rule Notes
Polar cancel_at_period_end === true Set by PATCH /v1/subscriptions/{id}.
Lemon Squeezy provider status is cancelled The status is the flag; the unified status stays active.
Stripe cancel_at_period_end === true || cancel_at !== null Both fields must be checked — see the note below.
Paddle scheduled_change?.action === 'cancel' Scheduling emits subscription.updated, not subscription.canceled.
Dodo Payments cancel_at_next_billing_date === true Forced to false once the status is terminal.

On Stripe and Dodo Payments, cancelAtPeriodEnd is normalized back to false when the subscription is terminally canceled — there is nothing left to schedule.

Each of these rules exists because a provider encodes the schedule somewhere different; normalizing subscription status across billing providers covers the entitlement bugs they cause when handled per provider.

pauseAtPeriodEnd detection

A pause scheduled for the end of the period behaves like a scheduled cancellation: the status stays unchanged and pauseAtPeriodEnd carries the schedule. Only the two providers that can schedule a pause ever set it.

Provider Rule Notes
Polar pause_at_period_end === true Polar pauses at period end only; set by PATCH /v1/subscriptions/{id}.
Lemon Squeezy always false Pauses take effect immediately.
Stripe always false pause_collection takes effect immediately.
Paddle scheduled_change?.action === 'pause' Both behaviors are supported; only the scheduled one sets the flag.
Dodo Payments always false No pause endpoint and no paused status.

resumesAt is the matching date field — when the subscription resumes automatically. It is absent for an indefinite pause.

subscriptionChange: the edge, not the level

Everything above is a level — the state a subscription is in right now. A webhook event can also carry the edge: WebhookEvent.subscriptionChange names the transition the provider’s event string named, so it fires exactly once instead of staying set for the rest of the period. Its per-provider coverage grid, with the exact event string behind every cell, is in Webhook events.

Date fields

Field Polar Lemon Squeezy Stripe Paddle Dodo Payments
currentPeriodStart current_period_start item current_period_start current_billing_period.starts_at previous_billing_date
currentPeriodEnd current_period_end renews_at item current_period_end current_billing_period.ends_at next_billing_date
trialEndsAt trial_end trial_ends_at trial_end item trial_dates.ends_at
resumesAt resumes_at pause.resumes_at pause_collection.resumes_at scheduled_change.resume_at while scheduled, else the resume change’s effective_at
startedAt started_at created_at start_date started_at created_at
endsAt ends_at ends_at cancel_at, else current_period_end when scheduled scheduled_change.effective_at when scheduled next_billing_date when scheduled, else expires_at
endedAt ended_at ends_at when expired ended_at canceled_at cancelled_at

endsAt answers “when does access end” — a scheduled cancellation date or the end of a grace period. endedAt answers “when did it actually terminate” and is only set once the subscription is over.

Checkout status mapping

Unified Polar Lemon Squeezy Stripe Paddle Dodo Payments
complete succeeded — (always null) complete and payment_status !== 'unpaid' paid, completed payment_status: succeeded
expired expired, failed expired canceled cancelled, failed
open everything else open, plus complete while payment_status === 'unpaid' everything else everything else

Lemon Squeezy checkouts carry no lifecycle status, so Checkout.status is always null (checkoutStatus: false).

Last updated on August 8, 2026

Was this page helpful?