What statuses can a Stripe subscription be in, and which transitions bite?
Lifecycle: incomplete → active (or trialing) → past_due → unpaid or canceled, plus incomplete_expired. incomplete means the first payment needs action (SCA) — it auto-expires after about 23-24 hours to incomplete_expired if never paid. past_due means a renewal failed but dunning retries are still running; per your retry settings it then goes to unpaid, canceled, or stays past_due while access continues. canceled is terminal — you can't un-cancel, you must create a new subscription. Paused subscriptions (via pause_collection) report status active, so check the pause_collection field separately. Map each status to an access decision in ONE place in your code; scattered status checks drift apart.
Upgrade mid-cycle: why is the invoice weird (proration)?
Changing a subscription's price by default creates prorations: Stripe credits unused time on the old price and bills the new price's remaining time, either on the next invoice or immediately. The proration_behavior parameter controls this: 'create_prorations' (default — line items now, charged next invoice), 'always_invoice' (charge immediately), or 'none' (no proration — simplest and often what SaaS actually wants for downgrades). Pitfalls: create_prorations + a failed next payment means upgrades effectively free until then; downgrading with create_prorations can leave a customer credit balance that offsets future invoices. Pending proration line items appear as upcoming invoice lines, confusing 'why is my next bill not the plan price' support tickets.
Cancel at period end vs cancel immediately — which API?
Two different mechanisms. Setting cancel_at_period_end=true on the subscription keeps it active until current_period_end, then cancels it — you get customer.subscription.updated with cancel_at_period_end=true when it's set, and customer.subscription.deleted only when the period actually ends. Deleting the subscription (DELETE /v1/subscriptions/:id) cancels immediately and fires customer.subscription.deleted right away; you can pass prorate/invoice_now options to issue a credit for unused time. Common bug: granting access 'until subscription.current_period_end' but revoking on the updated event, cutting off users who paid through the end of the month. Revoke access on deleted (or when period end passes), not on the flag change.
How do trials without a card work, and when does trial_end fire?
You can create a subscription with trial_end (Unix timestamp, seconds) and no payment method — status becomes trialing. Stripe fires customer.subscription.trial_will_end about 3 days before trial end (only if the subscription will attempt payment after), which is your 'add a card' prompt trigger. When the trial ends: if a payment method exists, Stripe invoices and on success fires invoice.paid plus subscription updated to active; if none exists, behavior depends on your settings — the subscription goes past_due or cancels. trial_end must be a whole-number Unix timestamp in SECONDS, not milliseconds — passing Date.now() puts the trial in year 56,000 and Stripe rejects it (or worse, you store garbage locally).
customer.subscription.updated fires constantly — which change do I care about?
It's a catch-all: plan changes, quantity changes, cancel_at_period_end flips, status transitions, even metadata edits all emit it, often several per action. Don't treat each one as a meaningful event. Diff against your stored copy: compare status, items (price ids, quantities), cancel_at_period_end, and current_period_end, and only act on fields you actually model. If you need causality, inspect the event's data.previous_attributes field — it lists exactly which attributes changed in that event, which is far more reliable than guessing from the payload. For pure state-sync use cases, ignore event semantics entirely and just upsert the subscription object as-is; reserve logic for deleted, trial_will_end, and invoice events.
Can I run a trial without collecting a card upfront?
Yes. Create the subscription with trial_end (or trial_period_days) and no payment method; with payment_behavior: 'default_incomplete' (the default) it starts in trialing while the trial runs. Control the no-card-at-trial-end case explicitly with trial_settings.end_behavior.missing_payment_method: 'cancel' cancels the subscription when the trial ends without a card — otherwise Stripe creates an invoice and the subscription slides into past_due dunning for a customer who never agreed to pay. Listen for customer.subscription.trial_will_end (about 3 days before) to prompt card collection via a SetupIntent or a setup-mode Checkout Session. Tradeoff: cardless trials convert better but invite abuse — disposable emails farming trials. Mitigate with email verification, CAPTCHA, and per-card-fingerprint repeat-trial blocking once you do collect cards.
Upgrade $10 to $50 mid-cycle — why is the invoice $30, not $40?
Proration math: Stripe credits the unused portion of the old plan and charges the remaining portion of the new one, roughly (days_remaining / days_in_cycle) × price_difference per line item, at second precision. Upgrade with 75% of the cycle left: 0.75 × $50 = $37.50 debit on the new price, 0.75 × $10 = $7.50 credit on the old, net $30 — the customer pays only the upgrade delta for the remaining time, not a full new period. Preview before committing: after staging the price change, fetch the upcoming invoice (invoice preview / preview-lines APIs) to show the user the exact prorated amount and line items. Downgrades work the same in reverse and can leave a customer credit balance that offsets future invoices.
Is 'inactive' a real Stripe subscription status?
No. As of early 2026 the complete status set is exactly seven: trialing, active, incomplete, incomplete_expired, past_due, unpaid, canceled. If a doc, library, or model mentions 'inactive', it is wrong — don't map it. Dangerous transitions to handle deliberately: incomplete → incomplete_expired (about 23 hours without the first payment — the subscription never really started); active/trialing → past_due on failed renewal (dunning running, access decision needed); past_due → unpaid or canceled per your retry settings (revoke access here); anything → canceled is terminal (no un-cancel, you must create a new subscription). Also note paused subscriptions (pause_collection) still report status active — check that field separately. Never blindly overwrite local state on customer.subscription.updated; diff or re-fetch.