On this page

Identity

GET /v1/whoami answers one question: what did the gateway decide my credential means? It takes no parameters, changes nothing, and returns the resolved scope — the caller, their role, their organization, department and project, and the grants that will be checked on every later call.

It is the first call to make when something is refused and you are not certain why, because almost every 403 is a disagreement between the credential you think you are sending and the one your client picked up.

The route

curl -sS https://fabric.wexa.ai/v1/whoami \
  -H "Authorization: Bearer $WEXA_API_KEY"
{
  "user_id": "u_abc",
  "role": "OWNER",
  "org_id": "org_abc",
  "dept_id": "dept_abc",
  "project_id": "proj_abc",
  "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"]
}
FieldMeaning
user_idThe person the credential acts as. For an API key this is whoever created it, not whoever is using it.
roleThe gateway's own role vocabulary: OWNER, ORG_ADMIN, ORG_MEMBER, PROJECT_ADMIN, PROJECT_MEMBER.
org_id, dept_id, project_idThe scope every call is pinned to. A tool call cannot reach outside it.
grantsThe permissions checked before each tool runs.
auth_kindWhich kind of credential was used: api_key, user_jwt, oauth or session.
session_idOnly for an SDK session from login: the session's id.

It accepts all four credential types. POST to the same path is 405 Method Not Allowed — it is a read, and the gateway keeps reads on GET. Without a credential it answers the standard 401 with the WWW-Authenticate challenge described in authentication.

The answer depends on the credential, not only on the person

This is the single most useful thing the endpoint tells you. One person, one project, three credentials, three different answers — all read back from a live gateway:

Credentialgrants
API key created with no explicit grants, by an OWNERquery.read, docs.read, ontology.write, orchestrate.read, orchestrate.write, agent.run, skill.write, model.write
User token for the same OWNERquery.read, ontology.write, docs.read, catalog.read, catalog.write, agent.run
OAuth access token consented to query.read docs.readquery.read, docs.read
API key created with {"grants":["fabric:query.read"]}query.read

Read the first two rows together. The user token is the only one of the three carrying catalog.read and catalog.write; the API key is the only one carrying skill.write and model.write. So a call that works from the dashboard can fail from a script, and the reverse, with the same person behind both. That is not a bug to work around — it is the answer to "why does this work there and not here", and whoami is where you see it.

The fourth row makes the other half of the point: role stayed OWNER on that narrow key while grants held one entry. The role is not the permission. A grant check reads the grant list, so an admin role on a key with one grant buys nothing.

Reading a refusal with it

A missing grant refuses at the scope-resolution stage and names the grant it wanted:

error (S2:resolve-scope): token missing required grant "fabric:ontology.write"

Compare that string against the grants array and the answer is immediate: either the credential was minted too narrow, or your client is not sending the credential you think it is. Nothing else needs debugging first.

When to prefer the documentation tool

whoami is deliberately minimal. For a fuller picture in one call, POST /v1/docs with {"topic":"governance"} returns the same grants plus your project's mode, your live quota headroom and the gateway's own operating rules:

{
  "grants": ["fabric:query.read", "…"],
  "mode": "auto",
  "quota": { "org_window_max": 600, "org_window_used": 1,
             "project_window_max": 120, "project_window_used": 1 },
  "scope": { "org_id": "org_abc", "dept_id": "dept_abc", "project_id": "proj_abc",
             "user_id": "u_abc", "role": "OWNER", "grants": ["…"],
             "auth_kind": "api_key", "api_key_id": "key_Qu8IVcZ6cScOU3psqTEFHxTt" }
}

The scope.auth_kind field is worth the call on its own: api_key, user_jwt or session tells you which credential your client actually picked up; whoami reports it too. Use whoami for a cheap scope check in a health endpoint or a startup assertion; use the documentation tool when you are debugging.

A startup assertion worth writing

An unattended job that asserts its own scope at startup fails on the deployment rather than halfway through a run, when half the work is already done:

import os, urllib.request, json

req = urllib.request.Request(
    f"{os.environ['WEXA_BASE']}/whoami",
    headers={"Authorization": f"Bearer {os.environ['WEXA_API_KEY']}"},
)
me = json.load(urllib.request.urlopen(req))

assert me["project_id"] == os.environ["EXPECTED_PROJECT"], f"wrong project: {me['project_id']}"
assert "fabric:ontology.write" in me["grants"], "key cannot write the ontology"

The second assertion is the one that earns its place. A key rotated with the role default instead of the grants the old one carried looks identical until the first write is refused — and by then the job has already read, computed and partly committed.