On this page

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.

RoutePurposeCredential
POST /v1/apikeysCreate a key and return its secret onceUser token, admin role
GET /v1/apikeys?project_id=…List the project's keys, without secretsUser token, admin role
DELETE /v1/apikeys/{id}?project_id=…Revoke one key immediatelyUser token, admin role

Create a key

POST /v1/apikeys — body is JSON.

FieldRequiredNotes
nameYesA 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_idYes in practiceRead from the body, not from your token's claims. Omitting it produces a key whose scope has no organization.
dept_idNoStamped into the key's scope as given.
project_idYesThe project the key is pinned to.
grantsNoAn 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, including ontology.write, agent.run, skill.write and model.write.
  • Every other role → query.read and docs.read only. 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

StatusBodyCause
400name is requiredname absent or blank.
400invalid_request with a decoder messageThe body is not valid JSON.
401invalid_tokenNo bearer header, or a credential that is not a valid user token — including an API key.
403API key creation requires an admin role on this projectA valid user token whose role is not OWNER, ORG_ADMIN or PROJECT_ADMIN. Confirmed live with a PROJECT_MEMBER token.
502identity_unavailableThe 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"}
StatusBodyCause
400project_id query param requiredThe query parameter is missing.
401invalid_tokenNot a user token, or not one at all.
403forbiddenValid user token, no admin role on that project.
404no such API key in this projectThe id is not in this project's list.
502revoke_failed / identity_unavailableThe 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:

  1. POST /v1/apikeys — mint the replacement, with the same grants rather than the role default if the old key was deliberately narrow.
  2. Deploy the new secret to every caller. Watch last_used_at on the old key stop moving.
  3. 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.