HeyIdentity Agent API: guide for AI agents
HeyIdentity is a hosted fake OpenID Connect / OAuth 2.0 identity provider for development and tests. The Agent API lets an AI agent create a working test tenant for the user's application, configure it to match the app, and hand it to the user to claim. Plain text for agents: /agent/llms.txt.
Workflow
- Read the catalog (GET /agent/v1/catalog).
- Create a tenant (POST /agent/v1/tenants) with a random, secret Idempotency-Key header and the current accepted_terms_version.
- Show the claim URL to the user right away, and save the agent token and claim URL to .heyidentity/tenant.json (git-ignored).
- Configure the tenant to match the application, one item per request: clients and redirect URIs, users, claims, settings.
- Validate the configuration (POST /agent/v1/configurations/{configuration_id}/validate).
Endpoints
Base URL https://api.heyidentity.dev. JSON only. Authenticate with Authorization: Bearer <agent token> (not needed for creation and the catalog).
Moved on 2026-10-24: the Agent API used to be served on the app host (app.<domain>). Its base URL is now https://api.heyidentity.dev/agent/v1, and the old paths answer 404 there. Tenants and agent tokens are unchanged.
GET /agent/v1/catalog | profiles, URL shapes, token shape options, standard claims, limits and the current terms (no token) |
POST /agent/v1/tenants | create a tenant: { accepted_terms_version, display_name?, profile? } with an Idempotency-Key header (no token); returns the agent token and claim URL |
GET /agent/v1/tenant | the tenant, its configurations and their endpoints |
POST /agent/v1/configurations | create another configuration: { name, profile? }; a retry with the same name answers configuration_exists |
GET /agent/v1/configurations/{configuration_id} | the configuration as items |
POST /agent/v1/configurations/{configuration_id}/validate | problems and warnings, without changing anything |
POST .../clients, PUT|DELETE .../clients/{client_id} | clients, redirect URIs and scopes |
POST .../users, PUT|DELETE .../users/{key} | fictional test users and their claims |
POST .../claims/custom, PUT|DELETE .../claims/custom/{name} | custom claims |
PUT .../claims/standard | enabled standard claims: { enabled: [...] } |
PUT .../settings | profile, token shape, token lifetime, client secret validation |
https://api.heyidentity.dev/agent/mcp | MCP server (streamable HTTP); its tools take agent_token as an argument. |
https://api.heyidentity.dev/agent/v1/openapi.json | OpenAPI 3.1 document of these endpoints; import it to call the API as REST tools. |
Request bodies
JSON objects with exactly these fields (snake_case); an unknown field is refused with 400 invalid_request naming it. On edits, fields left out keep their value. The catalog's request_shapes has the same as JSON schemas.
POST /agent/v1/tenants (create_tenant)
accepted_terms_version | string, required | The current Terms of Use version (catalog terms.current_version), accepted on behalf of the operator running the agent. |
display_name | string | The tenant's display name, at most 200 characters. Default: Agent test tenant. |
profile | string | Generic (default) or Microsoft (an Entra ID drop-in). |
POST /agent/v1/configurations (create_configuration)
name | string, required | The configuration's name, at most 200 characters and unique in the tenant ignoring case, e.g. staging. A retry with the same name answers configuration_exists with the existing configuration. |
profile | string | Generic (default) or Microsoft (an Entra ID drop-in). |
POST /agent/v1/configurations/{configuration_id}/clients (add_client)
client_id | string, required | The client id. Required when adding; fixed afterwards (it is the id in the URL when editing). |
client_secret | string | The client secret; checked only when the configuration's validate_client_secrets setting is on. |
allow_all_return_urls | boolean | Accept any redirect URI. Convenient for local tests; default false. |
redirect_uris | array of string | Exact redirect URIs the app uses, e.g. http://localhost:5173/callback (http(s) with a host, or an app scheme such as com.example.app:/callback or myapp://callback). |
allowed_scopes | array of string | Scopes the client may request, e.g. openid, profile, email, offline_access, api://orders/read. Default when adding: openid, profile. |
exposed_scopes | array of string | Scopes this client exposes as an API (resource), e.g. api://orders/read. Tokens for these scopes get this client as audience. |
access_token_audience | string | Access token audience when the request names no API scope; the client id when not set. |
PUT /agent/v1/configurations/{configuration_id}/clients/{client_id} (edit_client)
client_id | string | The client id. Required when adding; fixed afterwards (it is the id in the URL when editing). |
client_secret | string | The client secret; checked only when the configuration's validate_client_secrets setting is on. |
allow_all_return_urls | boolean | Accept any redirect URI. Convenient for local tests; default false. |
redirect_uris | array of string | Exact redirect URIs the app uses, e.g. http://localhost:5173/callback (http(s) with a host, or an app scheme such as com.example.app:/callback or myapp://callback). |
allowed_scopes | array of string | Scopes the client may request, e.g. openid, profile, email, offline_access, api://orders/read. Default when adding: openid, profile. |
exposed_scopes | array of string | Scopes this client exposes as an API (resource), e.g. api://orders/read. Tokens for these scopes get this client as audience. |
access_token_audience | string | Access token audience when the request names no API scope; the client id when not set. |
POST /agent/v1/configurations/{configuration_id}/users (add_user)
key | string, required | The user key shown on the sign-in page and used as sub by default. Required when adding; fixed afterwards. |
display_name | string | The name shown on the sign-in page and emitted as the name claim. |
sub | string | The sub claim; the key when not set. |
icon | string | A built-in icon id for the sign-in page. |
access_token_sub | string | A different sub for access tokens only. |
claims | object | The user's claims: name to a string, number, boolean or array of strings, e.g. { "email": "alice@example.com", "roles": ["admin"] }. Use fictional people only. |
claim_targets | object | Which token each claim goes into: name to both (default), id_token or access_token. |
PUT /agent/v1/configurations/{configuration_id}/users/{key} (edit_user)
key | string | The user key shown on the sign-in page and used as sub by default. Required when adding; fixed afterwards. |
display_name | string | The name shown on the sign-in page and emitted as the name claim. |
sub | string | The sub claim; the key when not set. |
icon | string | A built-in icon id for the sign-in page. |
access_token_sub | string | A different sub for access tokens only. |
claims | object | The user's claims: name to a string, number, boolean or array of strings, e.g. { "email": "alice@example.com", "roles": ["admin"] }. Use fictional people only. |
claim_targets | object | Which token each claim goes into: name to both (default), id_token or access_token. |
POST /agent/v1/configurations/{configuration_id}/claims/custom (add_custom_claim)
name | string, required | The claim name. Required when adding; fixed afterwards. |
value | string | The claim value. |
target | string | both (default), id_token or access_token. |
PUT /agent/v1/configurations/{configuration_id}/claims/custom/{name} (edit_custom_claim)
name | string | The claim name. Required when adding; fixed afterwards. |
value | string | The claim value. |
target | string | both (default), id_token or access_token. |
PUT /agent/v1/configurations/{configuration_id}/claims/standard (set_standard_claims)
enabled | array of string, required | The standard claims to enable, e.g. ["auth_time", "jti"]. Required. |
PUT /agent/v1/configurations/{configuration_id}/settings (edit_settings)
profile | string | Generic or Microsoft. |
validate_client_secrets | boolean | Require client secrets at the token endpoint. |
token_lifetime_minutes | string | Token lifetime in minutes (positive). |
token_shape | object | Claim shape options (Generic profile only); fields left out keep their value. |
Terms
Current Terms of Use version: 2026-10-23 (Terms of Use, Privacy Notice).
Creating a tenant accepts these terms on behalf of the operator running the agent: send accepted_terms_version with the current version. The operator is responsible for using fictional test people only, and agent tokens are the operator's secret.
Rules
- Right after creating a tenant, show its claim URL to the user with the ready-to-use message in next_steps: an unclaimed tenant and its configuration are deleted when it expires. Repeat it whenever a response's first next_steps entry is show_user.
- Save the agent token and the claim URL to .heyidentity/tenant.json and add .heyidentity/ to .gitignore. The agent token is the operator's secret: never commit it or put it in a URL.
- Never ask the user for their email address and never send one to the Agent API: the user claims the tenant themselves by opening the claim URL.
- Use only fictional test people in users and claims, with email-shaped values on reserved test domains (for example alice@example.com, bob@example.org). Never use real people's names, emails or identifiers, even when modelling a real organisation.
- Send an Idempotency-Key header when creating a tenant: a random secret of 32 to 128 characters from A-Z a-z 0-9 - _ (for example crypto.randomUUID(), uuidgen or openssl rand -hex 16), generated once per intended tenant and kept secret like the agent token. Retry only with the same key and body, and only if you never received the response. Never retry creation in a loop.
- Read the catalog (GET /agent/v1/catalog) before configuring a tenant to match an application: it describes the profiles, URL shapes, token shape options and standard claims.
- Prefer a separate client per environment (for example web-local, web-staging) within one configuration. Create another configuration (POST /agent/v1/configurations, with a name unique in the tenant) only for a different profile or a separate set of users. Only the user deletes configurations, on their management page. Only the user deletes tenants, on their management page after claiming; agents never delete either. The user can rename a configuration on their management page; keep its id, not its name, to find it again.
Errors and what to do next
| Status | Error | Meaning | Next |
|---|---|---|---|
| 429 | rate_limited | A per-source, per-token or global limit was hit, or too many unclaimed tenants exist. | Wait for the Retry-After time, then continue. Never retry creation in a loop; reuse your tenant. |
| 410 | tenant_expired | The tenant expired unclaimed and was deleted with its configuration. | Create a new tenant, show its claim URL to the user right away, and update the application's configuration. |
| 410 | tenant_deleted | The user deleted the tenant on its management page; deleted_at says when. It can't be restored. | Stop and ask the user whether they want a new tenant before creating one; never create one automatically. |
| 403 | not_supported | Agents cannot delete configurations or tenants. | Ask the user to delete the configuration, or the whole tenant, on their management page. |
| 400 | usage_limit_exceeded | A limit of the tenant's tier was reached; limit names it and its maximum. The tier follows the plan (pro, enterprise) and otherwise the assurance level. | Remove an unused item, or ask the user to claim the tenant, which raises the limits. A claimed tenant on the free plan gets higher limits with the Pro plan (next_steps links to the plans). At the configuration limit of a claimed tenant, the user can delete a configuration they no longer need on their management page. |
| 400 | invalid_request / invalid_item | A field is missing or invalid; field names it. | Fix that field and send the request again. |
| 400 | terms_not_accepted | accepted_terms_version is missing or not current. | Read the terms at terms_url and send current_terms_version. |
| 400 | email_not_accepted | The request contained an email address. | Remove it; the user claims the tenant on the claim page. |
| 401 | invalid_token | The agent token is missing, unknown or revoked. | Use the saved token. If it was lost or revoked, the tenant owner issues a new one on the management page after claiming. |
| 404 | not_found | No configuration with this id belongs to your tenant, or there is no Agent API endpoint at that path. | GET /agent/v1/tenant lists your configurations; the catalog's endpoints list the API. |
| 409 | already_claimed | A creation retry's tenant was already claimed, so the first response was received. | Use the agent token you saved. |
| 409 | idempotency_key_spent | The tenant created with this Idempotency-Key already received its response and its agent token has been used; nothing was issued. | Use the agent token you saved. If it is lost, create a new tenant with a new key. |
| 409 | concurrent_change | The tenant changed while a creation retry was processed; nothing changed. | Retry once more with the same Idempotency-Key and body. |
| 409 | configuration_exists | The tenant already has a configuration with this name (ignoring case); existing_configuration describes it and nothing was created. | Reuse that configuration if its profile is the one you need; otherwise choose another name. |
| 409 | configuration_unloadable | This configuration can't be edited as items. | Ask the user to delete it on their management page, or to contact support. |
| 405 | method_not_allowed | The path exists but not with this HTTP method. | Add items with POST, edit them with PUT (fields left out keep their value) and remove them with DELETE. |
| 415 | unsupported_media_type | The request body is not JSON. | Send the body as JSON with Content-Type: application/json. |
| 422 | idempotency_key_reused | The Idempotency-Key was used with a different body. | Use a new key for a different request. |
Lifecycle of an unverified tenant
A tenant you create is unverified and expires 7 days after creation. It works right away (discovery, sign-in, tokens) under the unverified limits. The user keeps it by opening the claim URL, entering their own email and confirming the link they receive; the tenant then has no expiry, gets the claimed limits, and your agent token keeps working. An unclaimed tenant is deleted with its configuration when it expires, and cannot be recovered. For 90 days its agent tokens get 410 tenant_expired; after that they get 401. After claiming, the user can also delete the tenant at any time on their management page; its agent tokens that were not revoked then get 410 tenant_deleted for 90 days, and 401 after that.
Limits
| Tier | Configurations | Clients | Users | Bytes | Tokens/min |
|---|---|---|---|---|---|
| unverified | 1 | 3 | 10 | 32768 | 20 |
| email_verified (claimed, free plan) | 5 | 5 | 20 | 65536 | 120 |
| pro plan (claimed) | 20 | 20 | 100 | 262144 | 300 |
| enterprise plan (granted by HeyIdentity on request) | 50 | 50 | 250 | 262144 | 1200 |
Agent API: 10 tenant creations per source per hour; 60 calls per agent token per minute. A source is one IPv4 address or one IPv6 /64, so agents that share an egress address (a NAT, shared CI runners) share these limits.
All limits and how to tell rejections apart: https://heyidentity.dev/docs/limits.html
What agents can't do
- Delete configurations: the user deletes them on the management page.
- Delete a tenant: the user deletes it on the management page after claiming; an unclaimed tenant is deleted when it expires.
- Revoke or list agent tokens: that is the tenant owner's, on the management page after claiming.
- Collect or send the user's email: the user claims the tenant on the claim page.