Skip to main content

Subscription lifecycle

After checkout, every state change is webhook-driven.

Events the marketplace cares about

Stripe eventMarketplace effect
checkout.session.completedUpsert subscription row, status from session
customer.subscription.createdSame; idempotent with the above
customer.subscription.updatedSync status, currentPeriodEnd, cancelAtPeriodEnd
customer.subscription.deletedstatus: 'canceled', canceledAt: now()
invoice.payment_succeededAudit-log only
invoice.payment_failedstatus: 'past_due'

Cancellation flow

Failed payment retry

The marketplace does not block access on past_due. The agent decides. Recommended:

  • Treat past_due as access-granted with a banner: "Payment failed. Update your card."
  • After ~7 days of past_due with no recovery, soft-block by treating it like canceled.

Status truth table for the agent

Marketplace statusAgent should treat asUI hint
trialingActive"Free trial — ends {date}"
activeActive(none)
past_dueActive (configurable)"Update payment method"
incompleteNOT active"Payment pending"
incomplete_expiredNOT active"Payment failed"
unpaidNOT active"Subscription unpaid"
canceledNOT active"Subscription ended"

Plan changes

When a user upgrades or downgrades:

Stripe handles proration. The marketplace just syncs the new tierId (derived from stripePriceId → matching agent_pricing_tiers row) on webhook.

Reconciliation

If a webhook is missed (Stripe retried but the backend was down for > 24h), use the operations script:

pnpm tsx scripts/reconcile-stripe.ts --agent-id=<id>

(Build this script if absent — it lists all Stripe subscriptions for the customer and ensures local rows are in sync.)

See also