Identity Management API
The Management API is rooted at /identity/admin on the environment’s control issuer. It accepts a bearer token issued by that control tenant for audience identity-admin-api.
Every request requires both its OAuth scope and the operator’s matching live directory permission. Removing the permission takes effect even while an earlier access token remains unexpired.
Authorization
| Scope | Operations |
|---|---|
identity.admin.read | overview, tenants, clients, resources, credentials, keys, federations and audit reads |
identity.admin.manage | tenant, client, resource, credential, key and federation mutations |
identity.admin.users.manage | user list, read, create, update and delete |
identity.admin.users.import | import creation, status, cancellation and record results |
identity.admin.users.export | export creation, status, cancellation, records and NDJSON download |
For terminal use, identityctl login opens an operator session and identityctl token refreshes that session before emitting an access token. Register a separate confidential administration client for unattended automation and grant only the scopes it needs.
Collection routes use the shared pagination contract.
Overview
GET /overview returns environment-wide totals.
Response — 200 OK
{
"tenants": 12,
"clients": 37,
"resources": 18,
"activeFederations": 4
}Tenants
| Method and route | Success |
|---|---|
GET /tenants | 200 page of tenants |
GET /tenants/{tenantId} | 200 tenant |
POST /tenants | 201 tenant |
PUT /tenants/{tenantId} | 200 updated tenant |
Creation accepts:
{
"slug": "acme",
"registrationMode": "CONTROLLED"
}registrationMode is CONTROLLED or SELF_SERVICE. Identity assigns the tenant ID and managed issuer during creation. A later update accepts slug, issuer and registrationMode, subject to the lock fields returned for the tenant.
Tenant response
{
"id": "f351511c-11e4-408f-b069-1c58baf673af",
"slug": "acme",
"issuer": "https://f351511c-11e4-408f-b069-1c58baf673af.auth.example.com",
"status": "ACTIVE",
"registrationMode": "CONTROLLED",
"clientCount": 2,
"resourceCount": 1,
"createdAt": "2026-10-13T08:42:11.384Z",
"managedIssuer": "https://f351511c-11e4-408f-b069-1c58baf673af.auth.example.com",
"configurationManaged": false,
"slugLocked": false,
"issuerLocked": false
}| Field | Meaning |
|---|---|
issuer | issuer currently exposed to clients |
managedIssuer | stable Identity-managed issuer that remains available when a custom domain is configured |
configurationManaged | this tenant is controlled by environment bootstrap and cannot be edited through this API |
slugLocked | the slug is referenced by another configuration and cannot currently change |
issuerLocked | the issuer cannot currently change |
status | ACTIVE or DISABLED |
Users
User reads and writes require identity.admin.users.manage.
| Method and route | Success |
|---|---|
GET /tenants/{tenantId}/users | 200 page of users |
GET /tenants/{tenantId}/users/{userId} | 200 user |
POST /tenants/{tenantId}/users | 201 user |
PUT /tenants/{tenantId}/users/{userId} | 200 updated user |
DELETE /tenants/{tenantId}/users/{userId} | 204 empty body |
CONTROL_ISSUER='https://auth.identity.lookandtry.com'
TENANT_ID='00000000-0000-0000-0000-000000000000'
curl --fail --silent --show-error \
-H "Authorization: Bearer $ACCESS_TOKEN" \
"$CONTROL_ISSUER/identity/admin/tenants/$TENANT_ID/users?page=0&size=25"Create accepts email and optional emailVerified, which defaults to false. Update replaces email, emailVerified and status, where status is ACTIVE or DISABLED.
User response
{
"id": "8b6e839c-114b-4242-ac95-7158a39fce39",
"subject": "usr_01JY7N8SV4B30TQGB8Y3YH5J9M",
"email": "ada@example.com",
"emailVerified": true,
"status": "ACTIVE",
"createdAt": "2026-10-13T08:42:11.384Z"
}Use subject, together with the tenant issuer, when another system needs to bind its account to this Identity user. id addresses the management resource and is used in the user route.
Resources
| Method and route | Success |
|---|---|
GET /tenants/{tenantId}/resources | 200 page of resources |
POST /tenants/{tenantId}/resources | 201 resource |
Creation accepts uri, audience and scopes.
{
"uri": "https://api.acme.example",
"audience": "acme-api",
"status": "ACTIVE",
"scopes": ["orders.read", "orders.write"]
}The resource URI is used by authorization requests. The audience is the value a receiving API expects in aud.
OAuth clients
| Method and route | Success |
|---|---|
GET /tenants/{tenantId}/clients | 200 page of client metadata |
POST /tenants/{tenantId}/clients | 201 issued client |
PUT /tenants/{tenantId}/clients/{clientId} | 200 issued client |
Create and update accept:
{
"clientId": "acme-web",
"displayName": "Acme web",
"applicationType": "WEB",
"tokenEndpointAuthMethod": "CLIENT_SECRET_BASIC",
"consentPolicy": "EXPLICIT",
"redirectUris": ["https://app.acme.example/callback"],
"scopes": ["openid", "email", "offline_access", "orders.read"],
"defaultResource": "https://api.acme.example",
"nativeAuthenticationEnabled": false
}applicationType is WEB or NATIVE. tokenEndpointAuthMethod is CLIENT_SECRET_BASIC or NONE. consentPolicy is EXPLICIT or PRE_AUTHORIZED.
Issuance response
{
"clientId": "acme-web",
"clientSecret": "cs_aF9m4vQ3..."
}clientSecret is null for a public client. When a secret is returned, capture it immediately; it is not available from list responses.
Client metadata in list responses
{
"clientId": "acme-web",
"displayName": "Acme web",
"applicationType": "WEB",
"tokenEndpointAuthMethod": "CLIENT_SECRET_BASIC",
"consentPolicy": "EXPLICIT",
"status": "ACTIVE",
"redirectUris": ["https://app.acme.example/callback"],
"scopes": ["email", "offline_access", "openid", "orders.read"],
"defaultResource": "https://api.acme.example",
"nativeAuthenticationEnabled": false,
"createdAt": "2026-10-13T08:47:20.118Z",
"configurationManaged": false
}Resource-server credentials
| Method and route | Success |
|---|---|
GET /tenants/{tenantId}/credentials | 200 page of credential metadata |
POST /tenants/{tenantId}/credentials | 201 issued credential |
DELETE /tenants/{tenantId}/credentials/{credentialId} | 204 empty body |
Issue a credential for a previously registered resourceUri:
{
"resourceUri": "https://api.acme.example"
}Issuance response
{
"credentialId": "rsc_4jWc2L...",
"credentialSecret": "rss_L7e9Qx..."
}Store credentialSecret immediately. List responses expose credentialId, resourceUri, status and createdAt, never the secret.
Signing keys
| Method and route | Success |
|---|---|
GET /tenants/{tenantId}/signing-keys | 200 page of signing-key metadata |
POST /tenants/{tenantId}/signing-keys | 201 staged key ID |
POST /tenants/{tenantId}/signing-keys/{keyId}/activation | 204 empty body |
POST /tenants/{tenantId}/signing-keys/{keyId}/retirement | 204 empty body |
A staged key response is:
{
"keyId": "key-2026-10"
}List entries contain keyId, state, createdAt, activatedAt and retiredAt. State is STAGED, ACTIVE, RETIRING, RETIRED or REVOKED. Activate before using a staged key to sign new tokens. Keep old public keys available long enough to validate tokens issued before rotation.
Federations and audit events
| Method and route | Success |
|---|---|
GET /federations | 200 page of federations |
POST /federations | 201 empty body |
DELETE /federations/{sourceTenantId}/{targetTenantId} | 204 empty body |
GET /audit-events | 200 page of audit events |
Federation creation accepts:
{
"sourceTenantId": "f351511c-11e4-408f-b069-1c58baf673af",
"targetTenantId": "3b85c18a-b2a4-4f0d-8b9d-624bd436ed11"
}A federation entry returns both tenant IDs, both issuers, status and createdAt. An audit event returns id, actorSubject, actorClientId, action, targetType, targetId, details and createdAt.
Bulk user imports and exports
Imports accept at most 500 records per request with schemaVersion: 1. The tuple (source, externalId) identifies one source-system account. idempotencyKey makes an exact retry return the same logical job and rejects a different payload using the same key. mode is CREATE_ONLY or VALIDATE_ONLY.
CONTROL_ISSUER='https://auth.identity.lookandtry.com'
TENANT_ID='00000000-0000-0000-0000-000000000000'
curl --fail --silent --show-error \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
--data '{
"schemaVersion": 1,
"source": "customer-backend",
"mode": "CREATE_ONLY",
"idempotencyKey": "migration-2026-10-13-part-1",
"records": [{
"externalId": "42",
"email": "user@example.com",
"emailVerified": true,
"active": true
}]
}' \
"$CONTROL_ISSUER/identity/admin/tenants/$TENANT_ID/user-imports"| Method and route | Success |
|---|---|
GET, POST /tenants/{tenantId}/user-imports | 200 page or 202 job |
GET, DELETE /tenants/{tenantId}/user-imports/{jobId} | 200 job or 204 cancellation |
GET /tenants/{tenantId}/user-imports/{jobId}/results | 200 page of record results |
GET, POST /tenants/{tenantId}/user-exports | 200 page or 202 job |
GET, DELETE /tenants/{tenantId}/user-exports/{jobId} | 200 job or 204 cancellation |
GET /tenants/{tenantId}/user-exports/{jobId}/records | 200 page of exported profiles |
GET /tenants/{tenantId}/user-exports/{jobId}/download | 200 application/x-ndjson |
Accepted job — 202 Accepted
{
"id": "e8b7c344-c409-4f7a-99c4-f21142cfaf18",
"tenantId": "f351511c-11e4-408f-b069-1c58baf673af",
"kind": "IMPORT",
"status": "QUEUED",
"source": "legacy-crm",
"mode": "CREATE_ONLY",
"totalItems": 2,
"processedItems": 0,
"succeededItems": 0,
"failedItems": 0,
"createdAt": "2026-10-13T09:00:00Z",
"startedAt": null,
"completedAt": null,
"lastFailure": null
}Poll the job until status is SUCCEEDED, COMPLETED_WITH_ERRORS, FAILED or CANCELLED. QUEUED and RUNNING are non-terminal.
Import result record
{
"position": 0,
"externalId": "legacy-42",
"email": "ada@example.com",
"status": "SUCCEEDED",
"subject": "usr_01JY7N8SV4B30TQGB8Y3YH5J9M",
"errorCode": null,
"processedAt": "2026-10-13T09:00:02.118Z"
}Store the externalId to subject mapping for any source system that must bind product accounts after migration. Failed records have status: "FAILED", a null subject and a machine-readable errorCode.
Export record and NDJSON
{
"position": 0,
"subject": "usr_01JY7N8SV4B30TQGB8Y3YH5J9M",
"email": "ada@example.com",
"emailVerified": true,
"status": "ACTIVE",
"createdAt": "2024-02-17T12:21:44Z"
}The download response contains one JSON object per line with the same export-record shape. Process it as a stream; do not load the entire file into memory.
An import record may carry a bcrypt password hash, a TOTP factor and upstream identities in addition to email state. See account migrations for the complete source export, import verification and product-binding workflow.
Errors
Management errors use {"error":"code"}.
| Status | Error | Meaning and client action |
|---|---|---|
400 | invalid_request | fix the page query, identifier, JSON body or unsupported value |
401 | invalid_token | obtain a live token from the control issuer for identity-admin-api |
403 | insufficient_scope | request the scope named by WWW-Authenticate |
403 | access_denied | grant the operator the matching live directory permission |
404 | administration_disabled | the Management API is disabled in this environment |
404 | unknown_tenant | the tenant ID does not exist |
404 | unknown_user | the user ID does not exist in the tenant |
404 | unknown_client | the client ID does not exist in the tenant |
404 | unknown_user_transfer | the import or export job ID does not exist in the tenant |
409 | tenant_conflict | the requested tenant conflicts with an existing tenant |
409 | tenant_issuer_locked | the issuer is already in use and cannot change |
409 | tenant_slug_locked | another configuration currently references the slug |
409 | configuration_managed | bootstrap configuration owns this resource |
409 | user_conflict | the email or account state conflicts with another user |
409 | user_transfer_conflict | the idempotency key was reused with a different transfer request |
409 | resource_conflict | the resource URI or audience conflicts with an existing resource |
409 | client_conflict | the client ID or client configuration conflicts with existing state |
409 | unknown_client_resource | the client’s default resource is not registered for the tenant |
409 | operation_conflict | the requested key, credential or federation transition is invalid in the current state |