## Goal

Operate the full recurring billing lifecycle: start checkout, confirm post-payment state, and provide self-service access with organization-level traceability.

## Endpoints and operational contract

| Endpoint                                                               | When to use                                 | Key requirements                                                 |
| ---------------------------------------------------------------------- | ------------------------------------------- | ---------------------------------------------------------------- |
| `POST /api/billing/subscription/checkout?org={slug}&mode={test\|live}` | start a new subscription or upgrade         | Beacon bearer token, `OWNER`/`ADMIN`, `plan`, and `billingCycle` |
| `POST /api/billing/subscription/confirm?org={slug}&mode={test\|live}`  | reconcile final state after checkout return | organization context; optional `sessionId` in the body           |
| `POST /api/billing/subscription/portal?org={slug}&mode={test\|live}`   | open self-service plan/payment management   | mapped Stripe customer and eligible subscription                 |

## Recommended flow

<Steps>
  <Step title="1) Create checkout">
    Create a session with plan, billing cycle, and organization context. Persist
    `checkoutSessionId`.
  </Step>
  <Step title="2) Redirect user">
    Send the user to `redirectUrl` and prevent concurrent submits in the
    frontend.
  </Step>
  <Step title="3) Confirm state">
    After the checkout return, call `subscription/confirm`. If you have the
    success URL `session_id`, pass it as `sessionId`.
  </Step>
  <Step title="4) Expose portal safely">
    Generate portal sessions only once a manageable Stripe customer/subscription
    exists.
  </Step>
</Steps>

## Checkout creation example

```bash
curl -X POST "https://test.beacon.pt/api/billing/subscription/checkout?org=acme&mode=test" \
  -H "Authorization: Bearer <beacon_session_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "plan": "plus",
    "billingCycle": "monthly"
  }'
```

```json
{
  "ok": true,
  "checkoutSessionId": "cs_123",
  "redirectUrl": "https://checkout.stripe.com/c/pay/cs_123",
  "plan": "PLUS",
  "billingCycle": "monthly",
  "context": {
    "mode": "test",
    "organizationSlug": "acme"
  }
}
```

## Confirmation and consistency

- Checkout does not expose a public `Idempotency-Key`; the Stripe idempotency key is generated server-side.
- If a pending checkout already exists for the same plan and cycle, the API can reuse it and return `pending: true`.
- If a different pending checkout exists or the organization already has an active subscription, the API returns `409`.
- Do not assume success from the redirect alone; final state must be confirmed by `subscription/confirm` and Stripe events.

## Example confirmation

```bash
curl -X POST "https://test.beacon.pt/api/billing/subscription/confirm?org=acme&mode=test" \
  -H "Authorization: Bearer <beacon_session_token>" \
  -H "Content-Type: application/json" \
  -d '{"sessionId":"cs_test_a1b2c3"}'
```

```json
{
  "ok": true,
  "subscription": {
    "plan": "PLUS",
    "status": "ACTIVE"
  },
  "context": {
    "mode": "test",
    "organizationSlug": "acme"
  }
}
```

## Frequent errors

| Code  | Typical cause                                                   | Recommended action                         |
| ----- | --------------------------------------------------------------- | ------------------------------------------ |
| `400` | invalid `org`, `plan`, or `billingCycle`                        | fix payload/query and retry                |
| `401` | invalid/expired token                                           | renew the Beacon session and retry         |
| `403` | caller is not `OWNER`/`ADMIN`                                   | review organization permissions            |
| `409` | active subscription, pending checkout, or session not ready yet | reuse existing state or wait a few seconds |
| `404` | organization missing or inactive                                | confirm slug and active context            |

## Go-live checklist

- Full test flow passes in `mode=test` with no pending states.
- Alerts configured for confirmation failures and retry spikes.
- Portal access restricted by role with audit trail.

<Note title="Best practice">Validate the full `checkout -> confirm -> webhook` flow before enabling `live`.</Note>
