On this page

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 keyUser tokenOAuth 2.1 access tokenSDK session
Looks likefab_sk_…A JWTA JWTA JWT, plus a fab_srt_… refresh token
Minted byPOST /v1/apikeysSign-inThe /oauth/authorize + /oauth/token exchangePOST /v1/auth/login with email and password
ScopePinned at mint time to one organization, department and projectThe session's own scopeThe scopes the person consented toOne project, as the logged-in user
LifetimeUntil revokedThe session's expiryThe token's expiry, refreshableOne hour, refreshable for 30 days
Reaches tool routesYesYesYesYes
Reaches /v1/apikeysNoYes, with an admin roleDepends on the granted scopesNo
Right forServers, jobs, anything unattendedA person's own session, and credential managementAn application acting for a personYour 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.