Projects
Create, list, update, disable, and delete projects and their guardrails.
A project groups API keys and carries the guardrails every key in it inherits: a model allowlist, a requests-per-minute limit, a spend limit and an IP allowlist. Guardrails are project-level — a key is never wider than its project.
| Endpoint | Scope | Token owner's role |
|---|---|---|
GET /projects | projects:read | Any member (members see their own projects) |
POST /projects | projects:write | Admin |
GET /projects/{id} | projects:read | Any member with access to the project |
PATCH /projects/{id} | projects:write | Admin |
POST /projects/{id}/disable | projects:write | Admin |
POST /projects/{id}/enable | projects:write | Admin |
DELETE /projects/{id} | projects:write | Admin |
projects:write is a sensitive scope.
Every write needs an owner who is currently an organization Admin; a
Member's token gets 403 insufficient_scope.
The Python examples on this page share this setup:
import os
import uuid
import requests
BASE = "https://portal.<domain>/api/v1/mgmt"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['TF_PAT']}"
def idem():
return {"Idempotency-Key": str(uuid.uuid4())}The project object
| Field | Type | Notes |
|---|---|---|
id | string | Opaque id. |
name | string | Display name. |
slug | string | URL-safe name, derived from the name at creation unless you pass one. |
description | string | null | |
status | string | active or disabled. |
is_default | boolean | true for the project created with the organization. |
created_at, updated_at | string | ISO-8601 UTC. |
monthly_spend_usd | number | Spend over the trailing 30 days, in USD. |
active_key_count | integer | On list rows. |
guardrails | object | The project's guardrails (below). Present on single-project responses (get, create, update, disable, enable); list rows omit it. |
guardrails field | Type | Notes |
|---|---|---|
model_allowlist | string[] | Models keys in this project may call (provider/model). Empty = every model your plan allows. Ids come from GET /catalog/models. |
rpm_limit | integer | null | Requests per minute for the project; null = no project limit. |
spend_limit_usd | number | null | Spend cap for the project in USD; null = no project cap. Cannot exceed the organization's spend_ceiling. |
ip_allowlist | string[] | CIDR ranges keys may be used from. Empty = any address. |
{
"id": "5b3e9a41-0c7d-4e2f-8a16-9d4b7c2e1f08",
"name": "payments",
"slug": "payments",
"description": "Payments service inference",
"status": "active",
"is_default": false,
"created_at": "2026-09-20T08:14:03Z",
"updated_at": "2026-09-22T11:30:55Z",
"monthly_spend_usd": 412.37,
"guardrails": {
"model_allowlist": ["openai/gpt-4o", "openai/gpt-4o-mini"],
"rpm_limit": 1200,
"spend_limit_usd": 2000,
"ip_allowlist": ["203.0.113.0/24"]
}
}List projects
GET /api/v1/mgmt/projectsReturns the projects the token owner can see, newest first: every project
for an Admin, the projects they belong to for a Member.
Paginated with limit and cursor.
| Query parameter | Notes |
|---|---|
status | active or disabled. Any other value → 422 validation_failed. |
curl -G https://portal.<domain>/api/v1/mgmt/projects \
-H "Authorization: Bearer $TF_PAT" \
-d limit=50{
"data": [
{
"id": "5b3e9a41-0c7d-4e2f-8a16-9d4b7c2e1f08",
"name": "payments",
"slug": "payments",
"description": "Payments service inference",
"status": "active",
"is_default": false,
"created_at": "2026-09-20T08:14:03Z",
"updated_at": "2026-09-22T11:30:55Z",
"monthly_spend_usd": 412.37,
"active_key_count": 2
}
],
"has_more": false,
"next_cursor": null
}Create a project
POST /api/v1/mgmt/projects| Body field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Display name. |
slug | string | no | Derived from name when omitted. |
description | string | no | |
region_id | string | no | An inactive or unknown region → 400, param: "region_id". |
guardrails | object | no | Any of model_allowlist, rpm_limit, spend_limit_usd. ip_allowlist cannot be set at creation — set it with PATCH afterwards. |
Requires Idempotency-Key. Returns 201 with the project object, including
its guardrails.
curl -X POST https://portal.<domain>/api/v1/mgmt/projects \
-H "Authorization: Bearer $TF_PAT" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1e2d3c-4b5a-4968-8776-5a4b3c2d1e0f" \
-d '{
"name": "payments",
"description": "Payments service inference",
"guardrails": {
"model_allowlist": ["openai/gpt-4o", "openai/gpt-4o-mini"],
"spend_limit_usd": 2000
}
}'Tip
Validate model ids against GET /catalog/models
before you write an allowlist. An allowlist naming a model the platform does
not know is rejected with 400 invalid_request, param: "guardrails.model_allowlist".
Retrieve a project
GET /api/v1/mgmt/projects/{id}Returns the project with its guardrails. A project in another organization —
or, for a Member, a project they do not belong to — returns 404, exactly
like one that does not exist.
curl https://portal.<domain>/api/v1/mgmt/projects/5b3e9a41-0c7d-4e2f-8a16-9d4b7c2e1f08 \
-H "Authorization: Bearer $TF_PAT"Update a project
PATCH /api/v1/mgmt/projects/{id}Changes the name, description and guardrails. Send at least one field. Only
the fields you send are changed; inside guardrails, each field you send
replaces the current value (send the full allowlist, not a delta) and
null clears it.
| Body field | Type | Notes |
|---|---|---|
name | string | Cannot be empty. The default project cannot be renamed (409 default_project_protected). |
description | string | null | |
guardrails.model_allowlist | string[] | null | [] or null clears the allowlist. |
guardrails.rpm_limit | integer | null | Not negative. null removes the project limit. |
guardrails.spend_limit_usd | number | null | Not negative. null removes the project cap. Must not exceed the org spend_ceiling. |
guardrails.ip_allowlist | string[] | null | CIDR notation. [] or null clears it. |
State is not part of PATCH: use the disable / enable verbs. One
request, one intent. Returns 200 with the project object. Guardrail changes
take effect within
60 seconds.
curl -X PATCH https://portal.<domain>/api/v1/mgmt/projects/5b3e9a41-0c7d-4e2f-8a16-9d4b7c2e1f08 \
-H "Authorization: Bearer $TF_PAT" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0a9b8c7d-6e5f-4a3b-9c2d-1e0f9a8b7c6d" \
-d '{"guardrails": {"spend_limit_usd": 2500, "ip_allowlist": ["203.0.113.0/24"]}}'Disable a project
POST /api/v1/mgmt/projects/{id}/disableThe reversible kill switch. Every key in the project stops
working for inference (their calls fail with
403 project_disabled); nothing else is touched, and no key is deleted. This is the
fastest way to stop a whole project — see Incident response.
No body. Returns 200 with the project object (status: "disabled"). The
default project cannot be disabled (409 default_project_protected).
curl -X POST https://portal.<domain>/api/v1/mgmt/projects/5b3e9a41-0c7d-4e2f-8a16-9d4b7c2e1f08/disable \
-H "Authorization: Bearer $TF_PAT" \
-H "Idempotency-Key: incident-4711-disable-payments"Enable a project
POST /api/v1/mgmt/projects/{id}/enableThe inverse of disable. Keys that were active before are active again. No
body. Returns 200 with the project object (status: "active").
curl -X POST https://portal.<domain>/api/v1/mgmt/projects/5b3e9a41-0c7d-4e2f-8a16-9d4b7c2e1f08/enable \
-H "Authorization: Bearer $TF_PAT" \
-H "Idempotency-Key: incident-4711-enable-payments"Delete a project
DELETE /api/v1/mgmt/projects/{id}Permanent and irreversible: the project, its settings and its
memberships are deleted, and its keys stop working. The one guard is the default project, which cannot
be deleted (409 default_project_protected); any other project can be
deleted whatever its state. No body. Returns 204 No Content.
curl -X DELETE https://portal.<domain>/api/v1/mgmt/projects/5b3e9a41-0c7d-4e2f-8a16-9d4b7c2e1f08 \
-H "Authorization: Bearer $TF_PAT" \
-H "Idempotency-Key: 1b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e"Warning
If you only need the keys to stop, disable the project instead — it is reversible. Delete is for projects you are sure you will never need again.
Errors specific to projects
| Status | Code | When |
|---|---|---|
| 400 | invalid_request (param set) | Missing or empty name; unknown field; negative rpm_limit/spend_limit_usd; a spend limit above the org spend_ceiling; an unknown model in model_allowlist; a bad CIDR; ip_allowlist on create; an invalid region_id. |
| 403 | insufficient_scope | Token lacks projects:read/projects:write, or (for writes) the owner is not currently an Admin. |
| 404 | not_found | Project absent, in another organization, or not visible to a Member owner. |
| 409 | default_project_protected | Renaming, disabling or deleting the default project. |
| 422 | unsupported_filter · validation_failed | An unknown query parameter, or a status filter value other than active/disabled. |