Skip to Content
API referenceManagement API

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.

Security boundary

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

ScopeOperations
identity.admin.readoverview, tenants, clients, resources, credentials, keys, federations and audit reads
identity.admin.managetenant, client, resource, credential, key and federation mutations
identity.admin.users.manageuser list, read, create, update and delete
identity.admin.users.importimport creation, status, cancellation and record results
identity.admin.users.exportexport 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 routeSuccess
GET /tenants200 page of tenants
GET /tenants/{tenantId}200 tenant
POST /tenants201 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 }
FieldMeaning
issuerissuer currently exposed to clients
managedIssuerstable Identity-managed issuer that remains available when a custom domain is configured
configurationManagedthis tenant is controlled by environment bootstrap and cannot be edited through this API
slugLockedthe slug is referenced by another configuration and cannot currently change
issuerLockedthe issuer cannot currently change
statusACTIVE or DISABLED

Users

User reads and writes require identity.admin.users.manage.

Method and routeSuccess
GET /tenants/{tenantId}/users200 page of users
GET /tenants/{tenantId}/users/{userId}200 user
POST /tenants/{tenantId}/users201 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 routeSuccess
GET /tenants/{tenantId}/resources200 page of resources
POST /tenants/{tenantId}/resources201 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 routeSuccess
GET /tenants/{tenantId}/clients200 page of client metadata
POST /tenants/{tenantId}/clients201 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 routeSuccess
GET /tenants/{tenantId}/credentials200 page of credential metadata
POST /tenants/{tenantId}/credentials201 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 routeSuccess
GET /tenants/{tenantId}/signing-keys200 page of signing-key metadata
POST /tenants/{tenantId}/signing-keys201 staged key ID
POST /tenants/{tenantId}/signing-keys/{keyId}/activation204 empty body
POST /tenants/{tenantId}/signing-keys/{keyId}/retirement204 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 routeSuccess
GET /federations200 page of federations
POST /federations201 empty body
DELETE /federations/{sourceTenantId}/{targetTenantId}204 empty body
GET /audit-events200 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 routeSuccess
GET, POST /tenants/{tenantId}/user-imports200 page or 202 job
GET, DELETE /tenants/{tenantId}/user-imports/{jobId}200 job or 204 cancellation
GET /tenants/{tenantId}/user-imports/{jobId}/results200 page of record results
GET, POST /tenants/{tenantId}/user-exports200 page or 202 job
GET, DELETE /tenants/{tenantId}/user-exports/{jobId}200 job or 204 cancellation
GET /tenants/{tenantId}/user-exports/{jobId}/records200 page of exported profiles
GET /tenants/{tenantId}/user-exports/{jobId}/download200 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"}.

StatusErrorMeaning and client action
400invalid_requestfix the page query, identifier, JSON body or unsupported value
401invalid_tokenobtain a live token from the control issuer for identity-admin-api
403insufficient_scoperequest the scope named by WWW-Authenticate
403access_deniedgrant the operator the matching live directory permission
404administration_disabledthe Management API is disabled in this environment
404unknown_tenantthe tenant ID does not exist
404unknown_userthe user ID does not exist in the tenant
404unknown_clientthe client ID does not exist in the tenant
404unknown_user_transferthe import or export job ID does not exist in the tenant
409tenant_conflictthe requested tenant conflicts with an existing tenant
409tenant_issuer_lockedthe issuer is already in use and cannot change
409tenant_slug_lockedanother configuration currently references the slug
409configuration_managedbootstrap configuration owns this resource
409user_conflictthe email or account state conflicts with another user
409user_transfer_conflictthe idempotency key was reused with a different transfer request
409resource_conflictthe resource URI or audience conflicts with an existing resource
409client_conflictthe client ID or client configuration conflicts with existing state
409unknown_client_resourcethe client’s default resource is not registered for the tenant
409operation_conflictthe requested key, credential or federation transition is invalid in the current state