# Subscriptions & plans Endpoints for plan discovery, sizing recommendations, and the subscription lifecycle. Backed by `plans.py` and `subscription.py`, using the `saas.plan`, `saas.subscription`, and `saas.configuration` models. ## `GET /api/plans` Public — no bearer token required. Returns the active plan catalog (used by the marketing pricing page as well as the portal). ## `GET /api/demo/plan` Public. Query param: `plan_id` (optional). Returns details for the trial plan (or a specific plan) used to pre-fill the demo signup flow. ## `GET /api/subscription/recommendation` Requires auth. Given desired sizing, returns a recommended plan plus a full price breakdown — this is what powers the "choose a plan" sizing calculator. **Query params:** | Param | Type | Default | Notes | |---|---|---|---| | `users` | int | `1` | Clamped to a minimum of 1 | | `storage` | float | `0` | GB requested beyond the plan's included storage | | `billing_cycle` | `"monthly"` \| `"yearly"` | `"monthly"` | Any other value falls back to `"monthly"` | **Response (200):** ```json { "success": true, "configuration_id": false, "requested": { "users": 5, "storage": 10, "billing_cycle": "monthly" }, "trial": { "available": true, "plan": {"...": "..."}, "pricing": {"...": "..."} }, "demo": { "available": true, "plan": {"...": "..."} }, "recommended_plan": {"...": "..."}, "pricing": { "plan_id": 3, "plan_name": "Growth", "users": 5, "storage": 10, "storage_limit": 20, "extra_storage": 0, "storage_overage_price": 0, "extra_storage_monthly": 0, "extra_storage_annual": 0, "base_monthly": 0, "base_annual": 0, "monthly_total": 0, "annual_total": 0, "annual_saving": 0, "annual_saving_percentage": 0, "effective_monthly_annual": 0, "billing_cycle": "monthly", "total": 0, "currency": "KES" } } ``` (Field values above are illustrative — see `_format_plan()` in `subscription.py` for the exact plan serialization, and the pricing dict construction just above this response block for the pricing math.) ## `GET /api/subscription/current` Requires auth. Returns the caller's tenant's active `saas.subscription` (plan, status, renewal date, configuration), or a `404`-style `{"success": false, ...}` if none exists. ## `POST /api/subscription/select-plan` Requires auth. Body: ```json { "plan_id": 3, "users": 5, "storage": 10, "workers": 2, "instances": 1, "billing_cycle": "monthly" } ``` Persists a `saas.configuration` record for the tenant ahead of checkout — this is the step the recurring `saas.configuration` "not persisted" bug (see [Troubleshooting](../../troubleshooting/api.md)) affects. ## `POST /api/subscription/subscribe` Requires auth. Body: `{"plan_id": 3, "billing_cycle": "monthly"}`. Creates or updates the tenant's `saas.subscription` against the selected plan. ## `POST /api/subscription/confirm` Requires auth. Body: `{"plan_id": 3}`. Finalizes the subscription after payment/checkout — call after M-Pesa (or other payment) confirmation. See [M-Pesa integration](../../integrations/mpesa.md). ## `POST /api/subscription/demo` Requires auth. Body: `{"plan_id": 3}`. Starts a time-boxed trial on the given (trial-flagged) plan without going through payment. :::note All five subscription-mutation routes only accept `POST`/`OPTIONS` — there's no `PUT`/`PATCH` variant; re-POST with new values to change a selection before checkout. :::