REST authentication
Every authenticated route takes one header:
Authorization: Bearer <credential>
The gateway accepts four kinds of credential behind that one header. They are not
interchangeable — each is minted differently, carries a different scope, and can reach a different
set of routes. Picking the wrong one produces a 401 that looks like a bad secret when it is
really the wrong kind of secret.
The four types
| API key | User token | OAuth 2.1 access token | SDK session | |
|---|---|---|---|---|
| Looks like | fab_sk_… | A JWT | A JWT | A JWT, plus a fab_srt_… refresh token |
| Minted by | POST /v1/apikeys | Sign-in | The /oauth/authorize + /oauth/token exchange | POST /v1/auth/login with email and password |
| Scope | Pinned at mint time to one organization, department and project | The session's own scope | The scopes the person consented to | One project, as the logged-in user |
| Lifetime | Until revoked | The session's expiry | The token's expiry, refreshable | One hour, refreshable for 30 days |
| Reaches tool routes | Yes | Yes | Yes | Yes |
Reaches /v1/apikeys | No | Yes, with an admin role | Depends on the granted scopes | No |
| Right for | Servers, jobs, anything unattended | A person's own session, and credential management | An application acting for a person | Your own login page, acting as the person who logged in |
The gateway records which one you used. POST /v1/docs reports it as auth_kind, and the value is
api_key, user_jwt or session — useful when a call behaves differently from how you expected and you are
not certain which credential your client picked up.
API keys — the default for anything unattended
An API key is the credential to reach for from a server, a scheduled job, or a developer machine. Its scope is fixed when it is minted and never widens, which is what makes it safe to hand to a process: a key cannot switch project, and a key cannot mint another key.
Minting one
Key creation requires a user token with an admin role on the target project — OWNER,
ORG_ADMIN or PROJECT_ADMIN. A lower role is refused with
API key creation requires an admin role on this project, and an API key presented here is refused
outright with 401 token invalid. That refusal is deliberate: if a key could mint a key, revoking
the first one would not contain the damage.
curl -s -X POST "$BASE/v1/apikeys" \
-H "Authorization: Bearer $USER_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"docs-c10","org_id":"org_c10","dept_id":"dept_c10","project_id":"proj_c10"}'
The response carries the secret exactly once:
{
"id": "key_jFN-7B_Qvj9JYz7FP2V_-4fi",
"name": "docs-c10",
"note": "store this secret now; it is not retrievable again",
"scope": {
"org_id": "org_c10", "dept_id": "dept_c10", "project_id": "proj_c10",
"user_id": "u_c10", "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_…"
}
Grants
When you request no explicit grants, the key gets the default set for the minter's role. An admin
role yields the eight-grant set shown above. Any other role — or an unrecognised one — fails closed
to the read-only pair, fabric:query.read and fabric:docs.read.
Explicit grants still win, so a deliberately narrow key stays narrow. A key can never do anything its minter could not already do.
Call POST /v1/docs with {"topic":"governance"} to see the grants a credential actually holds
rather than the ones you intended it to hold.
Listing and revoking
Both take project_id as a query parameter and both require a user token with an admin role on that
project.
curl -s "$BASE/v1/apikeys?project_id=proj_c10" -H "Authorization: Bearer $USER_TOKEN"
curl -s -X DELETE "$BASE/v1/apikeys/key_jFN-7B_Qvj9JYz7FP2V_-4fi?project_id=proj_c10" \
-H "Authorization: Bearer $USER_TOKEN"
Revocation is immediate. The next call with that key gets
401 {"error":"invalid_token","error_description":"api key invalid or revoked"}.
User tokens
A user token is a signed JWT carrying the caller's identity and scope: sub, role, org_id,
dept_id, project_id, and token_use. It is what a signed-in person's own session holds.
It reaches tool routes as well as credential management. Its grants come from its own claims, or
from the default set for its role when it carries none — which means a user token and an API key
minted by the same person can end up with different effective grants. Check with
POST /v1/docs rather than assuming.
Use a user token for credential management and for a person acting as themselves. Do not embed one in a service: it expires with the session, and its scope is the person's rather than the job's.
OAuth 2.1
OAuth is the path for an application acting on a person's behalf, including an editor or desktop assistant connecting over the Wexa MCP server. The gateway is both the resource server and its own authorization server, and it publishes two discovery documents so a client can configure itself:
curl -s "$BASE/.well-known/oauth-protected-resource"
curl -s "$BASE/.well-known/oauth-authorization-server"
The authorization-server document reports:
{
"issuer": "http://localhost:7123",
"authorization_endpoint": "http://localhost:7123/oauth/authorize",
"token_endpoint": "http://localhost:7123/oauth/token",
"registration_endpoint": "http://localhost:7123/oauth/register",
"grant_types_supported": ["authorization_code", "refresh_token"],
"response_types_supported": ["code"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none", "client_secret_post", "client_secret_basic"]
}
Authorization code with PKCE (S256) is the only code flow offered, and dynamic client registration
is available at /oauth/register, so a client can onboard without anyone pre-registering it.
The scopes both documents advertise are the grant names themselves:
fabric:query.read fabric:ontology.write fabric:docs.read fabric:codesync.write
fabric:agent.run fabric:orchestrate.read fabric:orchestrate.write
What a missing or bad credential looks like
Sending nothing:
HTTP/1.1 401 Unauthorized
Www-Authenticate: Bearer resource_metadata="http://localhost:7123/.well-known/oauth-protected-resource"
{"error":"invalid_token","error_description":"missing bearer credential"}
The WWW-Authenticate header points at the protected-resource document, so a compliant client can
discover where to authenticate without being told. Sending a bad or revoked key gives the same
status with api key invalid or revoked; sending a credential of the wrong kind for the route gives
token invalid.
Both SDKs map every one of these to AuthError. See
error handling for the rest of the hierarchy.
Environment variables
Both SDKs read the same two variables, and using the same names for a curl session keeps every
sample on this site copy-pasteable:
export WEXA_WORKSPACE=https://fabric.wexa.ai
export WEXA_API_KEY=fab_sk_…
WEXA_WORKSPACE is the workspace URL, not the API base. The API base comes from
GET /v1/connection-info.