> Source: https://wexa.ai/docs/sdks-and-apis/identity

# Identity

`GET /v1/whoami` returns what the gateway decided your credential means: the caller, their role, organization, department and project, and the grants checked on every later call. Make it the first call when something is refused and you are not sure why. It changes nothing and accepts all four credential types.

**Read your resolved scope**

**REST API**

```bash
curl -sS https://fabric.wexa.ai/v1/whoami \
  -H "Authorization: Bearer $WEXA_API_KEY"
```

```json
{
  "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"]
}
```

**TypeScript SDK**

```ts
import { Fabric } from '@wexa-fabric/sdk'

const fabric = new Fabric()
const result = await fabric.whoami()
```

**Python SDK**

```python
from wexa import Fabric

fabric = Fabric()
me = fabric.whoami()
```

## Parameters

| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| none | | | This endpoint takes no parameters. Send any of the four credential types as a bearer token. |

**Errors**

| Status | Code | Raised when |
| --- | --- | --- |
| 405 | `method_not_allowed` | The request is a `POST` to `/v1/whoami`. It is a read, and the gateway keeps reads on `GET`. |
| 401 | `invalid_token` | No credential was sent. The answer is the standard `401` with the `WWW-Authenticate` challenge described in [authentication](/docs/get-started/authentication). |

**More details**

**Response fields**

| Field | Meaning |
|---|---|
| `user_id` | The person the credential acts as. For an API key this is whoever created it, not whoever is using it. |
| `role` | The gateway's own role vocabulary: `OWNER`, `ORG_ADMIN`, `ORG_MEMBER`, `PROJECT_ADMIN`, `PROJECT_MEMBER`. |
| `org_id`, `dept_id`, `project_id` | The scope every call is pinned to. A tool call cannot reach outside it. |
| `grants` | The permissions checked before each tool runs. |
| `auth_kind` | Which kind of credential was used: `api_key`, `user_jwt`, `oauth` or `session`. |
| `session_id` | Only for an SDK session from [login](/docs/sdks-and-apis/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](/docs/get-started/authentication).

**How the answer depends on the credential**

### 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:

| Credential | `grants` |
|---|---|
| API key created with no explicit grants, by an `OWNER` | `query.read`, `docs.read`, `ontology.write`, `orchestrate.read`, `orchestrate.write`, `agent.run`, `skill.write`, `model.write` |
| User token for the same `OWNER` | `query.read`, `ontology.write`, `docs.read`, `catalog.read`, `catalog.write`, `agent.run` |
| OAuth access token consented to `query.read docs.read` | `query.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:

```text
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.

**The documentation tool, and a startup assertion**

### 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:

```json
{
  "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:

```python
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.

**Related**

- [Authentication](/docs/get-started/authentication): How each credential type resolves to the scope this endpoint echoes.
- [API keys](/docs/get-started/authentication#create-an-api-key): Minting a key with exactly the grants the job needs.
- [Discovery](/docs/sdks-and-apis/discovery): The uncredentialed counterpart, for clients that hold nothing yet.
- [Core ideas](/docs/get-started/core-ideas#know-where-your-work-lives): What an organization, department and project pin actually mean.

Every tool call works the same way: [How every tool call works](/docs/sdks-and-apis/how-every-tool-call-works).
