Delegated Identity (API Integration)
Governing per-employee AI usage from a product that holds one API key.
This guide is for teams integrating their own product with AgentWatch: a SaaS platform, an internal portal, or an agent/workflow runtime that calls upstream LLMs on behalf of many end users.
1. The problem
Section titled “1. The problem”Your product registers employees (CSV, Okta/SCIM, or SSO) and then runs work for them — a cron job, an agent, a chain of sub-agents — that calls an LLM. You want AgentWatch to show which employee that spend, that prompt, and that DLP verdict belonged to.
There are two ways to tell AgentWatch who the user is, and they suit different architectures:
| Per-employee key | Delegated identity | |
|---|---|---|
| What your backend stores | one ak_ per employee |
one service key |
| Per-request header needed | none | yes — the identity header |
| Works with any LLM client | yes (identity rides in Authorization) |
needs a client that can set a header |
| Employee shown in Usage Insights | ✅ | ✅ |
| Per-employee budget / rate limit / DLP policy | ✅ | ✅ |
You cannot have both “one secret” and “no header” — anything that claimed otherwise would be guessing at who the user was. Pick the row that matches how much secret material you are willing to hold.
A middle path many teams prefer: one service key per workload or per customer-tenant (dozens, not thousands) plus the identity header for the human. Small enough to manage, and it still puts a hard boundary between your customers’ organizations.
2. Your employees never need to receive a key
Section titled “2. Your employees never need to receive a key”This is the part that surprises most integrators: AgentWatch mints the employee’s gateway key server-side and never puts it on the wire.
- SCIM 2.0 —
POST /scim/v2/<org-id>/Userscreates the user, provisions their tenant, and mints theirak_key. The SCIM response is a standard SCIM user object: no key in the payload. - CSV import —
POST /admin/tenants/import-csvdoes the same for a bulk onboarding; the response carries only{line, id, email, status}. - SSO — a first login provisions the user. With
autoCreateUsers=falsethe user landsssoStatus='pending'and no key is minted until a tenant admin approves them (POST /api/tenant/sso/approve-user).
So you can provision thousands of governable identities without your product
ever receiving, transporting, or storing a per-employee secret. Those identities
are what makes X-Zscaler-User: john.doe@acme.com mean something.
3. Marking a key as a service key
Section titled “3. Marking a key as a service key”Every API key is a personal key by default — it is one employee, and it cannot act as anyone else. Nothing about an existing integration changes until an admin deliberately reclassifies a key.
In the admin UI (recommended)
Section titled “In the admin UI (recommended)”Sign in as a tenant admin → Employee Management:
- Add Service Account Key creates a non-human account whose key is a service key straight away. Copy the key from the API Key column (copy / show buttons) and hand it to your backend.
- The Key Type column on every row switches an existing key between Personal, Service — collect all, Service — require identity, and Service — Auto Create Users (§4a). The change takes effect on the next request.
Or over the API
Section titled “Or over the API”Get a tenant-admin JWT first (24h, revocable on logout):
curl -s -X POST https://<your-agentwatch-host>/api/auth/login \ -H 'Content-Type: application/json' \ -d '{"email":"admin@yourcompany.com","password":"…","slug":"<your-org-slug>"}'# → {"token":"eyJhbGciOi…", …} ← use as: Authorization: Bearer <token>## `slug` scopes the login to your organization. It is the same token the admin# UI uses, so anything you can do in Employee Management you can do over the API.Then:
PUT /admin/keys/identity-policyAuthorization: Bearer <tenant-admin JWT>Content-Type: application/json
{ "tenantId": "<the key owner's tenant id>", "keyType": "service", "attributionMode": "attribute-to-key", "label": "acme-prod backend"}| Field | Values | Meaning |
|---|---|---|
keyType |
personal (default) · service |
Is this key one human, or does it act for humans? |
attributionMode |
attribute-to-key (default) · require-identity |
What a service key does when a request carries no identity header |
unknownIdentity |
reject (default) · auto-create |
What a service key does when the header names somebody who does not exist yet (§4a) |
autoCreateDomains |
["acme.com", …] |
Required with auto-create. The only domains that key may create employees in |
autoCreateAnyDomain |
true · false (default) |
Drops the allowlist entirely (§4b) |
GET /admin/keys/identity-policy lists every classified key in your
organization, along with the identity header name this deployment is configured
with. A tenant admin can only classify keys inside their own org.
Tip — make the service key its own identity. Create a service-account user
(e.g. svc-acme-prod@yourcompany.com) and use its key as the backend
credential. It then has the same rotate / suspend / resume lifecycle as any
employee, and un-delegated traffic shows up as a clearly named actor rather than
inside a human’s usage.
4. Calling the gateway
Section titled “4. Calling the gateway”POST /v1/chat/completionsAuthorization: Bearer ak_<your one service key>X-Zscaler-User: john.doe@acme.com ← who this call is for
X-Agent-Run-Id: run_2026-08-28T03:00_invoice-recon ← stitches the whole runx-aw-tag-agent: invoice-reconcilerx-aw-tag-parent-agent: orchestratorx-aw-tag-step: 3X-Session-Id: <conversation id> ← optional, groups turns- The identity header name is set by the
USER_HEADERenvironment variable (defaultX-Zscaler-User).X-AgentWatch-Useris always accepted as an alias, so an API integration never has to send a vendor-branded header. X-Agent-Run-Idgroups every LLM call in one agent run — parent and sub-agents alike — under a singleagent_run_idyou can filter on. If you don’t send one, AgentWatch derives a run id from the key plus a 15-minute window so uninstrumented agents still group.x-aw-tag-<key>headers need no configuration. Up to 12 tags per request; keys ≤64 chars, values ≤128.
What happens for every combination
Section titled “What happens for every combination”| Key type | Identity header | Result |
|---|---|---|
| personal | absent | 200 — attributed to the key’s owner (unchanged behavior) |
| personal | its own email | 200 — benign echo |
| personal | someone else | 403 identity_not_permitted + audit alert |
| service | absent, attribute-to-key |
200 — attributed to the service identity. Telemetry is never dropped |
| service | absent, require-identity |
401 identity_required |
| service | unknown to this org | 401 identity_unknown — never fails open |
| service | in a different org | 403 identity_cross_org |
| service | a login with no gateway identity | 403 identity_not_provisioned — provision them first |
| service | suspended / disabled | 403 identity_suspended |
| service | provisioned in this org | 200 — attributed to that employee |
| service + Auto Create Users | unknown, domain on the allowlist | 200 — the employee is created, then attributed to (§4a) |
| service + Auto Create Users | unknown, domain NOT on the allowlist | 403 identity_auto_create_refused (reason: not-allowlisted) |
| service + Auto Create Users (any domain) | unknown, any domain | 200 — the employee is created and attributed to (§4b) |
Two guarantees worth stating plainly:
- The org comes from the key, never from the header. A delegated request is resolved strictly inside the service key’s organization, so one customer’s key can never attribute usage to another customer’s employee.
- Delegation moves the whole governance stack, not just the label. Rate limits, budgets, and DLP policy for a delegated request are the named employee’s, because everything downstream reads the same resolved tenant.
4a. Auto Create Users — governing people you haven’t enrolled yet
Section titled “4a. Auto Create Users — governing people you haven’t enrolled yet”By default a service key can only act for employees that already exist: an
address it has never seen is 401 identity_unknown, because attributing usage
to somebody with no record would produce rows nothing downstream can govern.
That is the right default, and the wrong one for an integrator whose employee
list lives in their product. Auto Create Users closes that gap: turn it on
for a service key, name the email domains it owns, and the first time the
identity header carries a new address on one of those domains, AgentWatch
creates that employee — a real user record with its own ak_ gateway identity,
budgets, rate limits, team and policy — and serves the request attributed to
them. No pre-enrolment step, no second secret.
They join the service account’s own team, so their spend rolls up exactly where that integration’s spend already does and they appear in every team filter from the first request. If the service account has no team, they join your organization’s default team (the team named “Default Team”, otherwise your oldest team, otherwise a “Default Team” created for you). Move anyone afterwards like any other employee — a person who already belongs to a team keeps it.
PUT /admin/keys/identity-policyAuthorization: Bearer <tenant-admin JWT>
{ "tenantId": "<the service key's tenant id>", "keyType": "service", "attributionMode": "attribute-to-key", "unknownIdentity": "auto-create", "autoCreateDomains": ["acme.com", "acme.co.uk"], "label": "acme-prod backend"}What it will and will not create. The identity header is a plain string on a credential your whole backend shares, so the feature is deliberately bounded:
| Bound | Behavior |
|---|---|
| Off by default | A key that has not opted in behaves exactly as before (401 identity_unknown) |
| Domain allowlist | Only addresses on a listed domain (or its subdomains). A wildcard * is refused — an empty or wildcard list is rejected by the API with AUTO_CREATE_REQUIRES_DOMAINS |
| Service keys only | A personal key can never reach this path |
| The org comes from the key | A created employee always lands in the service key’s organization. The header carries no org and cannot imply one |
| Plan seat limit | Enforced exactly as in Add employee — the gateway is not a side door around maxUsers |
| Rate cap | 50 new employees per organization per hour (AW_AUTO_CREATE_MAX_PER_HOUR); over that, 429 with reason: rate-limited |
| Never for administrators | An address that resolves to an admin or super-admin login is refused (reason: privileged) |
| Never for pending SSO users | Somebody an admin deliberately left awaiting approval stays pending (reason: sso-pending) |
| No login is created | The account has no usable password. It is a governed gateway identity; reset the password if that person should also get console access |
Every creation is written to Settings → Audit Logs as
security.identity_auto_created, naming the address, the service key that
caused it, and the client IP. Refusals appear in the identity-alert feed (§6)
like every other rejected assertion. In Usage Insights, an auto-created
employee’s first request carries the tag identity_auto_created, so you can
always ask “which of these people did the gateway create for us, and when”.
When NOT to use it. If your employee list is already in an IdP, SCIM or a CSV import gives you joiner/leaver handling, roles and team assignment that a gateway header cannot. Auto Create Users is for the case where your product is the system of record and pre-enrolment is the thing you are trying to avoid.
4b. Auto Create Users — any domain (unrestricted)
Section titled “4b. Auto Create Users — any domain (unrestricted)”Your end users will not all share one domain. Contractors, consultants, agency staff, BYOD and personal addresses all legitimately join a tenant’s traffic, and they cannot be enumerated up front. So a tenant admin can select Service — Auto Create Users (any domain) in the Key Type column for a key in their own organization, or send:
PUT /admin/keys/identity-policy{ "tenantId": "<the service key's tenant id>", "keyType": "service", "attributionMode": "attribute-to-key", "unknownIdentity": "auto-create", "autoCreateAnyDomain": true}"autoCreateDomains": ["*"] is accepted as shorthand for the same thing. The row
is then marked ANY DOMAIN in the console so nobody has to read a policy blob
to discover it.
This needs no super-admin. It is the same authority you already hold over your own employee list: an SSO provider’s Auto-create users with no domain restriction provisions anyone who authenticates through your IdP, and you can add any-domain employees by hand or by CSV import. Setting a key in another organization still requires a super-admin, as it does for every other field on that endpoint.
This removes one bound — the domain allowlist. Everything else in the table above still applies, unchanged: the address must still be a valid email, the employee still lands in the service key’s organization, the plan seat limit and the 50/hour cap are still enforced, administrators and pending-SSO logins are still never auto-created, and every creation is still audited.
⚠ Understand what you are turning on: every address your backend puts in the
identity header becomes an employee, whatever its domain — including a typo or
an address that is not yours. Each one consumes a seat against maxUsers, and
the hourly cap means a runaway integration is throttled rather than stopped.
Prefer §4a’s allowlist whenever you can list the domains; use this when you
genuinely cannot.
Trust model
Section titled “Trust model”Your product asserts who the user is; AgentWatch trusts that assertion because your backend is the authenticated party holding the service key. This is the same trust model as a Zscaler or Netskope proxy stamping the corporate user on outbound traffic — which is why the header is named the way it is. Keep the service key server-side; never ship it to a browser or a desktop client.
5. What you see in Usage Insights
Section titled “5. What you see in Usage Insights”- Employee column — the delegated employee, exactly as if they had used their own key.
- Audit trail — filter by header/tag:
agent_run_idfor one workflow run,identity_mode(delegated/service) to separate machine-driven traffic from human traffic,identity_viafor “everything my backend sent”. - Usage by Tag —
GET /admin/analytics/usage-by-tag?tagKey=agent_run_idaggregates cost and tokens for a whole agent run. - DLP violations — attributed to the employee whose prompt triggered them.
Multi-call agent runs are grouped as a filterable set today. A visual parent→child trace tree is not built yet; Observe’s “distributed trace” view is a per-request waterfall (gateway → DLP → routing → provider).
6. Security monitoring
Section titled “6. Security monitoring”Every rejected identity assertion is recorded:
GET /admin/alerts/identity— the recent-rejections feed for your org (impersonation attempts, unknown identities, cross-org attempts, suspended users, missing-required identity).- Settings → Audit Logs — a durable
security.*entry for every rejection, and for every change to a key’s identity policy. The durable write always happens; only the dashboard feed and webhooks are throttled, so a misbehaving client can’t flood your alert list. - Webhook —
security.suspicious_activityfires with the key, the asserted identity, the client IP, and the path.
An impersonation attempt is a real signal: it means a credential that is supposed to represent one person tried to speak for another. Alert on it.
7. Configuration reference
Section titled “7. Configuration reference”| Setting | Where | Default |
|---|---|---|
| Identity header name | USER_HEADER env |
X-Zscaler-User (X-AgentWatch-User always accepted) |
| Key classification | PUT /admin/keys/identity-policy |
personal |
| Missing-identity behavior | attributionMode |
attribute-to-key |
| Unknown-identity behavior | unknownIdentity |
reject |
| Domains Auto Create Users may use | autoCreateDomains |
(empty — creates nothing) |
| Any-domain (unrestricted) mode | autoCreateAnyDomain (tenant admin, own org) |
false |
| Auto Create Users cap | AW_AUTO_CREATE_MAX_PER_HOUR env |
50 per org per hour |
| Alert de-duplication window | AW_IDENTITY_ALERT_THROTTLE_MS env |
300000 (5 min) |
Policy is stored in system_settings — no SQL schema change, so adopting
this is a plain image upgrade.
8. Troubleshooting
Section titled “8. Troubleshooting”| Symptom | Cause | Fix |
|---|---|---|
403 identity_not_permitted |
the key is still personal |
classify it as a service key |
401 identity_unknown |
the employee isn’t provisioned in the key’s org | provision via SCIM / CSV / admin API first, or switch the key to Auto Create Users (§4a) |
403 identity_auto_create_refused, reason: not-allowlisted |
Auto Create Users is on, but that address’s domain isn’t listed | add the domain to autoCreateDomains |
429 identity_auto_create_refused, reason: rate-limited |
more than 50 new employees in an hour | bulk-provision the rest with SCIM / CSV, or raise AW_AUTO_CREATE_MAX_PER_HOUR |
403 identity_auto_create_refused, reason: seat-limit |
the org is at its plan’s maxUsers |
upgrade the plan or remove unused employees |
403 identity_auto_create_refused, reason: privileged |
the address is an admin login | administrators are never auto-created; provision them explicitly |
400 AUTO_CREATE_REQUIRES_DOMAINS |
auto-create was sent with no domain list |
name the domains, or set autoCreateAnyDomain: true |
403 Access denied on the policy endpoint |
the key belongs to a different organization | only a super-admin can reclassify another org’s key |
403 identity_not_provisioned |
the user can log in but was never given a gateway identity (common for admin/SSO-only accounts) | provision them via SCIM / CSV / admin API |
401 identity_required |
key is require-identity and the header was missing |
send the header, or switch to attribute-to-key |
| All usage lands on one row | header not reaching the gateway | confirm the client forwards custom headers; try X-AgentWatch-User |
| Policy change didn’t take effect | 30s policy cache | wait 30s (immediate on a single-replica deployment) |