Skip to content
Docs

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.


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>/Users creates the user, provisions their tenant, and mints their ak_ key. The SCIM response is a standard SCIM user object: no key in the payload.
  • CSV import — POST /admin/tenants/import-csv does the same for a bulk onboarding; the response carries only {line, id, email, status}.
  • SSO — a first login provisions the user. With autoCreateUsers=false the user lands ssoStatus='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.


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.

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.

Get a tenant-admin JWT first (24h, revocable on logout):

Terminal window
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-policy
Authorization: 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.


POST /v1/chat/completions
Authorization: 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 run
x-aw-tag-agent: invoice-reconciler
x-aw-tag-parent-agent: orchestrator
x-aw-tag-step: 3
X-Session-Id: <conversation id> ← optional, groups turns
  • The identity header name is set by the USER_HEADER environment variable (default X-Zscaler-User). X-AgentWatch-User is always accepted as an alias, so an API integration never has to send a vendor-branded header.
  • X-Agent-Run-Id groups every LLM call in one agent run — parent and sub-agents alike — under a single agent_run_id you 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.
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:

  1. 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.
  2. 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-policy
Authorization: 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.

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.


  • Employee column — the delegated employee, exactly as if they had used their own key.
  • Audit trail — filter by header/tag: agent_run_id for one workflow run, identity_mode (delegated / service) to separate machine-driven traffic from human traffic, identity_via for “everything my backend sent”.
  • Usage by Tag — GET /admin/analytics/usage-by-tag?tagKey=agent_run_id aggregates 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).


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_activity fires 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.


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.


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)