Authentication
One key per project, plus the user each request acts for.
The project key
Each project in your organization has its own key, issued by the console on the project's API key tab. It works on the services enabled on that project, and nowhere else. It's a signed token (RS256) that each service verifies against the console's public keys, so no service has to call the console to check it.
The key carries only facts that don't change until it's regenerated: that it's a project key, the project's ID, your organization's ID, the key's ID, its issuer, the services it's for and its expiry. Nothing about any user is in it. Anyone can read these facts; only the console can sign them.
Your organization
├── Project A → key for Agent Platform and Knowledge
├── Project B → key for Recommendation
└── Project C → key for Knowledge and CrawlerEach service keeps every project's data apart, even within one organization: project B's key can't see project A's documents, agents or users.
Every request sends the key as a bearer token:
Authorization: Bearer <project key>Enabling a service on a project
A key lists the services enabled when it was made. After you enable another service on a project, regenerate the key so it works there too.
The acting user
Because the key names only a project, requests also say who they act for:
| Header | Sent by | Meaning |
|---|---|---|
X-User-Id | Agent Platform, Crawler, Knowledge, Recommendation, and Data Brain uploads | UUID of the user in your system. Required by these services |
X-User-Role | the same four, optional | the user's role. Recommendation lets admin, owner and service_role write and train |
X-User-Scopes | Knowledge only, optional | comma-separated scopes that narrow what the user can see. Omit for project-wide; send it empty for nothing |
Your backend holds the key, so the services trust it to name the user truthfully. That's the reason the key must never reach a browser: anyone holding it can act as any user of that project.
Data Brain checks the key and X-User-Id only on file uploads, which land in the project's own storage; its other routes take no key. KYC uses its own service key.
One user per client
Set the user when you build the client. Fine for scripts and single-user jobs:
from aice_recommendation import RecommendationClient
client = RecommendationClient(
base_url=os.environ["AICE_RECO_URL"],
api_key=os.environ["AICE_API_KEY"],
user_id=user.id,
user_role=user.role,
)A different user per request
A web backend serves many users at once. The two languages solve this differently.
Wrap the call in acting_as. It's scoped to the current thread or asyncio task, so one client and its connection pool serve concurrent users safely:
from aice_core import acting_as
with acting_as(request.user.id, role=request.user.role):
recs = client.recommend("user-1", n=10)acting_as overrides the user the client was built with, for that block only.
The Agent Platform client fixes its user at construction
It wraps the LangGraph SDK, which bakes its headers in when the client is built, so acting_as doesn't reach its streaming and thread calls. Build one Agent Platform client per user. See Agent Platform authentication.
Scope checks in the client
If a key carried a scope claim, the SDK would read it (without verifying it) and raise InsufficientScopeError before sending a request to a service outside it. Project keys carry no scopes, so this check never fires for them and the server answers instead. It's a convenience, never a security boundary.
Rotation and revocation
Services verify keys by signature. Regenerating or revoking a key in the console therefore doesn't stop a key that has already been issued until it expires. Treat a leaked key as live until its expiry, and regenerate it straight away anyway so it stops being handed out.
Rules
- Read the key from the environment or a secret store. Never commit it.
- Never send it from a browser or a mobile app. Put an AICE feature behind your own backend.
- Never log
Authorizationheaders. - Don't treat the client-side scope check as access control.