Platform admin API
The staff routes that set prices and plans.
Cross-team routes for the platform operator. Every route answers 401 without a
session and 403 unless the user is a platform admin. GET /api/v1/user/me
returns is_platform_admin so the dashboard can show the control panel.
A user is a platform admin when users.is_platform_admin is set, or when their
email is listed in the API's PLATFORM_ADMIN_EMAILS (comma-separated). A listed
email is flagged on its first request and cannot be revoked through the API.
| Method | Path | Does |
|---|---|---|
GET | /api/v1/admin/teams | Every team, suspended included, with owner_email, member_count, project_count |
GET | /api/v1/admin/teams/{teamID} | One team with members and projects |
POST | /api/v1/admin/teams/{teamID}/suspend | Members get 403 team is suspended on every team route. Running services, webhook builds and scheduled jobs carry on |
POST | /api/v1/admin/teams/{teamID}/unsuspend | Reinstates the team |
GET | /api/v1/admin/projects?health= | Every live project with its latest build and deployment and a derived health (failed, in_progress, running, idle), failed first |
DELETE | /api/v1/admin/projects/{projectID} | Deletes through the same path as the team route: soft delete, webhook cleanup, teardown job |
GET | /api/v1/admin/users | Every user with team_count, is_platform_admin, is_bootstrap_admin |
PUT | /api/v1/admin/users/{userID}/platform-admin | Grants platform admin |
DELETE | /api/v1/admin/users/{userID}/platform-admin | Revokes it. 409 for the last admin or a PLATFORM_ADMIN_EMAILS admin |
GET | /api/v1/admin/audit?limit=&offset= | Actions taken through these routes |
GET | /api/v1/admin/prices | Every plan with its fee, limits, usage rates and active; rates, the usage rates every plan charges (null when they differ); each bundle the platform ships with its apps, price_cents (null until it has one), its lower price (null until staff set one), offered, and guide: what its apps cost a month on their own, on the cheapest plan a team may choose; each add-on with its id, name, kind, days, price_cents and offered; limits; and waf_available |
GET | /api/v1/admin/prices/changes?limit=&offset= | Every price change, newest first: the plan, bundle, add-on or limits, the field, old_value, new_value, who made it and when |
PUT | /api/v1/admin/prices/plans/{planID} | Sets a plan's price_cents, ram_gb, vcpu and max_projects |
PUT | /api/v1/admin/prices/rates | Sets gb_hour_cents and vcpu_hour_cents on every plan |
PUT | /api/v1/admin/prices/bundles/{bundleID} | Sets a bundle's price_cents, its lower price and offered. A bundle without a price is not offered. A lower price left out keeps the one the bundle has, and once set it is never cleared |
PUT | /api/v1/admin/prices/addons/{addonID} | Sets an add-on's price_cents, offered, and for a backup period days, which must be more than every app includes. The firewall cannot be offered while waf_available is false |
PUT | /api/v1/admin/prices/limits | Sets smallest_topup_cents, largest_topup_cents and included_backup_days, which is never below the 7 days every copy is kept |
Every mutating admin route writes an audit row with metadata.platform_admin = true. Rows about a team carry its team_id, so they also appear in that team's
own audit log; grants, revokes and price changes carry none.
A price change also lands in the price history, one row per value it changed,
with who made it. It applies from each team's next charge, and an add-on's reaches
new purchases only: a purchase keeps the price it was bought at. The PUTs answer
with the prices as GET /api/v1/admin/prices does, and a value the plans or
bundle prices cannot hold with 422 and a fields object.