Stripe portal
Subscription management, invoices, and top-up purchases.
All subscription and top-up actions route through the Stripe Customer Portal. Hatched does not ship its own billing UI — the portal is the single source of truth for payment methods, invoices, plan switches, and credit bundle purchases.
Open the portal
From the dashboard: Billing → Manage billing opens a portal session scoped to the signed-in customer. Under the hood:
POST /api/v1/billing/portal
Authorization: Bearer <dashboard-jwt>
Content-Type: application/json
{ "flow": "default" }{ "portal_url": "https://billing.stripe.com/p/session/…" }flow can be:
default— the full portal (subscription, invoices, payment method, credit add-ons)top_up— deep-link to the credit bundle add-on flowcancel— deep-link to the cancel confirm
Subscription checkout (for new customers)
Free plan customers upgrading to Growth or Pro:
POST /api/v1/billing/checkout
Content-Type: application/json
{ "flow": "subscription", "plan": "growth" }{ "checkout_url": "https://checkout.stripe.com/c/pay/cs_…" }One-off credit bundle
Top-ups use a one-off Stripe Checkout session (or the portal's add-on UI).
POST /api/v1/billing/checkout
Content-Type: application/json
{ "flow": "credit_bundle", "credit_bundle": "100" }Valid bundle keys: "100", "500", "1000".
The checkout.session.completed webhook (mode=payment) grants credits into
the paid pool atomically, keyed on stripe_event_id so a double delivery
is a no-op.
Webhook handling
Hatched subscribes to the following Stripe events:
| Event | Effect |
|---|---|
checkout.session.completed (subscription) | Set customer.plan, grant the plan's included credits — full 12-month allotment upfront for annual, one month's worth for monthly. |
checkout.session.completed (payment) | Top-up: split the bundle into paid pool (non-expiring) + promo pool (bonus credits, expire after bonus_expires_days). |
invoice.payment_succeeded (subscription_cycle) | Grant the plan's included credits for the cycle — 12 months upfront on annual renewal, one month on monthly renewal. |
invoice.payment_failed | Set billing_status = past_due. |
customer.subscription.updated | Reconcile plan and status from Stripe truth. |
customer.subscription.deleted | Downgrade to starter, keep paid credits. |
charge.refunded | Logged; manual credit reversal required for top-ups. |
All credit grants are idempotent on stripe_event_id via
credit_transactions.uq_credit_tx_stripe_event.
Stripe product setup
One-time setup in the Stripe dashboard:
- Create two recurring products for plans:
Hatched Growth Monthly→price_growth_monthly→ envSTRIPE_GROWTH_PRICE_IDHatched Growth Annual→price_growth_annual→ envSTRIPE_GROWTH_ANNUAL_PRICE_IDHatched Pro Monthly→price_pro_monthly→ envSTRIPE_PRO_PRICE_IDHatched Pro Annual→price_pro_annual→ envSTRIPE_PRO_ANNUAL_PRICE_ID
- Create four one-off products for top-ups:
- 100 credits · $10 → env
STRIPE_CREDITS_100_PRICE_ID - 500 credits + 50 bonus · $50 → env
STRIPE_CREDITS_500_PRICE_ID - 1,000 credits + 150 bonus · $99 → env
STRIPE_CREDITS_1000_PRICE_ID - 2,500 credits + 500 bonus · $249 → env
STRIPE_CREDITS_2500_PRICE_ID
- 100 credits · $10 → env
- In Customer Portal, enable subscription update (Growth ↔ Pro), cancel,
invoice history, and customer-initiated one-off purchases scoped to
the four credit bundle products. Save the configuration id to
STRIPE_PORTAL_CONFIGURATION_ID.