API keys
Three routes manage fab_sk_ credentials. All three require a user token with an admin role on
the target project, and none of them can be reached with an API key. This page is the per-route
reference; authentication explains where a key sits among the three
credential types.
| Route | Purpose | Credential |
|---|---|---|
POST /v1/apikeys | Create a key and return its secret once | User token, admin role |
GET /v1/apikeys?project_id=… | List the project's keys, without secrets | User token, admin role |
DELETE /v1/apikeys/{id}?project_id=… | Revoke one key immediately | User token, admin role |
Create a key
POST /v1/apikeys — body is JSON.
| Field | Required | Notes |
|---|---|---|
name | Yes | A human label. Empty or missing is refused 400 name is required. The id is random and carries no meaning, so this is the only way to tell keys apart later. |
org_id | Yes in practice | Read from the body, not from your token's claims. Omitting it produces a key whose scope has no organization. |
dept_id | No | Stamped into the key's scope as given. |
project_id | Yes | The project the key is pinned to. |
grants | No | An explicit grant list. Omitted, the key inherits the default set for the caller's role. |
curl -sS -X POST https://fabric.wexa.ai/v1/apikeys \
-H "Authorization: Bearer $USER_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"nightly-report","org_id":"org_abc","dept_id":"dept_abc","project_id":"proj_abc"}'
201 Created:
{
"id": "key_IEhcUZrVvqI9C9hOAkwA_yOM",
"name": "nightly-report",
"note": "store this secret now; it is not retrievable again",
"scope": {
"org_id": "org_abc", "dept_id": "dept_abc", "project_id": "proj_abc",
"user_id": "u_abc", "role": "OWNER",
"grants": ["fabric:query.read", "fabric:docs.read", "fabric:ontology.write",
"fabric:orchestrate.read", "fabric:orchestrate.write", "fabric:agent.run",
"fabric:skill.write", "fabric:model.write"],
"auth_kind": "api_key"
},
"secret": "fab_sk_EoWX5uQBDWVJfxXhgGdJOygW"
}
The grants a new key gets
With no grants field the key inherits the default set for the creating role, not a fixed
starter pair:
OWNER,ORG_ADMIN,PROJECT_ADMIN→ eight grants, includingontology.write,agent.run,skill.writeandmodel.write.- Every other role →
query.readanddocs.readonly. That branch is unreachable here in practice, because a non-admin cannot create a key at all.
An explicit list is honoured exactly. Creating a key with {"grants":["fabric:query.read"]} and
then reading GET /v1/whoami with it returned precisely one grant — and kept "role": "OWNER" in
the scope, because the role is copied from the creator while the grants are not. Read the grants,
not the role.
Narrow keys are the right default for anything unattended. A key cannot exceed what its creator held, but it can very easily hold more than the job needs.
Refusals
| Status | Body | Cause |
|---|---|---|
400 | name is required | name absent or blank. |
400 | invalid_request with a decoder message | The body is not valid JSON. |
401 | invalid_token | No bearer header, or a credential that is not a valid user token — including an API key. |
403 | API key creation requires an admin role on this project | A valid user token whose role is not OWNER, ORG_ADMIN or PROJECT_ADMIN. Confirmed live with a PROJECT_MEMBER token. |
502 | identity_unavailable | The token carried no role claim, so the gateway had to look up the caller's project role and could not. Membership is never taken on trust from the request. |
List keys
GET /v1/apikeys?project_id=…. The query parameter is required — without it the call is refused
400 project_id query param required before authentication is even attempted.
curl -sS "https://fabric.wexa.ai/v1/apikeys?project_id=proj_abc" \
-H "Authorization: Bearer $USER_TOKEN"
{
"keys": [
{
"id": "key_IEhcUZrVvqI9C9hOAkwA_yOM",
"name": "nightly-report",
"scope": { "org_id": "org_abc", "project_id": "proj_abc", "role": "OWNER", "grants": ["…"], "auth_kind": "api_key" },
"created_by": "u_abc",
"created_at": "2026-09-15T15:52:30.732832779+05:30",
"last_used_at": "2026-09-15T15:52:41.582623887+05:30",
"revoked": false
}
]
}
last_used_at is the audit field worth watching. A key with no recent use is a key you can revoke
cheaply; a revoked key that keeps being used tells you which caller still has the old secret. The
secret itself is absent from every entry, and revoked keys stay listed rather than disappearing,
so the record of what existed survives the revocation.
Revoke a key
DELETE /v1/apikeys/{id}?project_id=…. Both the path id and the query parameter are required.
curl -sS -X DELETE "https://fabric.wexa.ai/v1/apikeys/key_IEhcUZrVvqI9C9hOAkwA_yOM?project_id=proj_abc" \
-H "Authorization: Bearer $USER_TOKEN"
{ "ok": true }
The id is checked against the project's own key list before anything is revoked, so a project admin
cannot revoke a key belonging to another project by guessing its id. An id that is not in this
project answers 404 no such API key in this project — the same answer whether the key exists
elsewhere or not at all.
Revocation takes effect on the next request. Immediately after the {"ok":true} above, the same
key on GET /v1/whoami answered:
HTTP/1.1 401 Unauthorized
{"error":"invalid_token","error_description":"api key invalid or revoked"}
| Status | Body | Cause |
|---|---|---|
400 | project_id query param required | The query parameter is missing. |
401 | invalid_token | Not a user token, or not one at all. |
403 | forbidden | Valid user token, no admin role on that project. |
404 | no such API key in this project | The id is not in this project's list. |
502 | revoke_failed / identity_unavailable | The key store, or the record of the caller's role, could not be reached. |
Rotating a key
There is no rotate endpoint. Rotation is three calls in this order, and the order is what keeps the caller running:
POST /v1/apikeys— mint the replacement, with the same grants rather than the role default if the old key was deliberately narrow.- Deploy the new secret to every caller. Watch
last_used_aton the old key stop moving. DELETE /v1/apikeys/{id}— revoke the old one.
Reversing steps 1 and 3 takes the integration down for the length of the deployment.
Audit
Creation and revocation both append an audit event — apikey.created and apikey.revoked, each
recording the actor, their role, the key id and the project. They are readable at GET /v1/audit;
see lifecycle and audit for the record's shape and its
hash-chain verification.