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):

{
  "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:

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

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.

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.