# Stripe portal

> Subscription management, invoices, and top-up purchases.

Source: https://docs.hatched.live/docs/billing/stripe-portal

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:

```http
POST /api/v1/billing/portal
Authorization: Bearer <dashboard-jwt>
Content-Type: application/json

{ "flow": "default" }
```

```jsonc
{ "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 flow
- `cancel` — deep-link to the cancel confirm

## Subscription checkout (for new customers)

Free plan customers upgrading to Growth or Pro:

```http
POST /api/v1/billing/checkout
Content-Type: application/json

{ "flow": "subscription", "plan": "growth" }
```

```jsonc
{ "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).

```http
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:

1. Create two recurring products for plans:
   - `Hatched Growth Monthly` → `price_growth_monthly` → env `STRIPE_GROWTH_PRICE_ID`
   - `Hatched Growth Annual` → `price_growth_annual` → env `STRIPE_GROWTH_ANNUAL_PRICE_ID`
   - `Hatched Pro Monthly` → `price_pro_monthly` → env `STRIPE_PRO_PRICE_ID`
   - `Hatched Pro Annual` → `price_pro_annual` → env `STRIPE_PRO_ANNUAL_PRICE_ID`
2. 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`
3. 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`.
