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, issue initial monthly credit grant. |
checkout.session.completed (payment) | Grant credits to paid pool (top-up metadata carries amount). |
invoice.payment_succeeded (subscription_cycle) | Grant monthly credit allotment for the plan. |
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→price_growth→ envSTRIPE_GROWTH_PRICE_IDHatched Pro→price_pro→ envSTRIPE_PRO_PRICE_ID
- Create three one-off products for top-ups:
- 100 credits · $10 → env
STRIPE_CREDITS_100_PRICE_ID - 500 credits · $50 → env
STRIPE_CREDITS_500_PRICE_ID - 1,000 credits · $99 → env
STRIPE_CREDITS_1000_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 three credit bundle products. Save the configuration id to
STRIPE_PORTAL_CONFIGURATION_ID.