Authentication
The project key, the acting user, and how the platform keeps each project's and user's conversations apart.
Every request carries the project's key and the user it acts for:
Authorization: Bearer <project key>
X-User-Id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
X-User-Role: admin (optional)The client sends these for you from api_key and user_id (apiKey and userId in TypeScript).
The acting user is required
Unlike some services, the Agent Platform rejects a request with no X-User-Id, and the value must be a UUID. It becomes the owner of every thread created with it, so use the stable ID from your own user table, never an email address or a display name.
X-User-Role is optional and passed through to the agent's policies.
One client per user
The conversation half of the client is the LangGraph SDK, which fixes its headers when the client is built. So:
acting_as(Python) doesn't change the user forthreads,runs,assistantsorcrons.- Build an
AgentPlatformClientfor each user you act for. Construction is cheap and makes no network call.
def agent_for(user) -> AgentPlatformClient:
return AgentPlatformClient(
base_url=settings.AICE_AGENT_URL,
api_key=settings.AICE_API_KEY,
user_id=str(user.id),
)How conversations are isolated
The platform stores every thread, run and saved item under an identity it computes itself from the verified key and header:
identity = "{project id}:{user id}"Nothing in a request body can change it. The consequences:
threads.search()returns only the calling user's threads. There's no parameter to widen it.- Reading another user's thread returns
404, not403. The platform won't even confirm that the ID exists. - The same user ID in two projects is two different people, even in the same organization. Agents, skills and usage are per project too.
What the platform checks
| Check | On failure |
|---|---|
Authorization: Bearer … is present | 401 missing bearer token |
| the key's signature verifies against the console's public keys (RS256 only) | 401 invalid token |
| issuer matches, and the key's services include the Agent Platform | 401 invalid token |
| the key hasn't expired (30 s clock leeway) | 401 token expired |
| it's a project key, naming a project and an organization | 401 not a project key / 401 invalid token |
X-User-Id is present and a UUID | 401 missing X-User-Id / 401 naming the header |
| the console's public keys could be fetched | 503: the check fails closed |
Only 401 token expired is worth handling at runtime: regenerate the key in the console. If every call is 401 invalid token, check that the project enables the Agent Platform and that the key was made after it was enabled. Every other 401 means the key or headers are wrong, and retrying won't fix it.
Call it from your backend
Because the key opens everything in its project, it must never reach a browser. A web or mobile chat talks to your backend, which authenticates your user in its own way and then calls the platform with the key and that user's ID. Build a chat UI shows the full pattern.
A useful side effect: server-to-server calls don't involve CORS, so there's nothing to configure on the platform for your frontend's origin.
The platform verifies keys against the console's JWKS. These settings must match the console that issues your keys:
| Setting | Meaning |
|---|---|
TENANT_KEY_JWKS or TENANT_KEY_JWKS_URL | the console's public keys: inline JSON (wins), or its /.well-known/jwks.json (a file:// path works for a local stack) |
TENANT_KEY_ISSUER | the console's issuer name (aice-console by default) |
TENANT_KEY_AUDIENCE | the audience this service accepts, agent-platform by default. The console's keys must carry it |
JWT_LEEWAY_SECONDS | clock skew allowance, 30 by default |
With either of the first two unset, every request is rejected.