Skip to main content

External Identifiers

This guide explains how to configure and use the external_id field in Ory Kratos to support external primary identifiers such as customer_id, employee_id, or similar. This is especially useful for migrations from systems where you need to preserve identifiers or support user-defined primary identifiers.

note

The external_id must be unique across all identities. If you attempt to import multiple identities with the same external_id, the operation will fail with a 409 Conflict.

Overview

Traditionally, Ory Kratos identifies users using an internal identity.id UUID. With the external_id feature, you can:

  • Assign a unique, domain-specific identifier to each identity.
  • Query and manage identities using external_id.
  • Use external_id as the sub (subject) claim in JWTs.
  • Preserve identity semantics across systems during migration.

This helps simplify migrations, reduce mapping layers, and align Kratos with your existing infrastructure.

Configuration

Use external_id via API, not schema

The external_id is not part of the identity JSON Schema. Instead, it is a dedicated top-level attribute in API requests that create or update identities.

Do not add external_id to your identity schema definition. It is handled separately by Ory Kratos internally.

Use external_id in JWT sub claim

Set the subject_source to external_id in the tokenization config:

session:
whoami:
tokenizer:
templates:
jwt_template_1:
jwks_url: base64://... # A JSON Web Key Set (required)
claims_mapper_url: base64://... # A JsonNet template for modifying the claims
ttl: 1m # 1 minute (defaults to 10 minutes)
subject_source: external_id
another_jwt_template:
jwks_url: base64://... # A JSON Web Key Set

This will populate the sub claim in JWTs with the value of external_id.

If external_id is not set for a user when subject_source is external_id, tokenization will fail.

API usage

Create an identity with external_id

POST /admin/identities
Content-Type: application/json

{
"schema_id": "default",
"traits": {
"email": "user@example.com"
},
"external_id": "customer-12345"
}

Get identity by external_id

GET /admin/identities/by/external/customer-12345

Optional query parameter

  • include_credential=password,oidc,... — Include specific credentials in the response.

Example:

GET /admin/identities/by/external/customer-12345?include_credential=password

Response:

{
"id": "uuid-abc123",
"external_id": "customer-12345",
"traits": {
"email": "user@example.com"
},
"credentials": {
"password": { ... }
}
}

Error responses:

  • 404 – Identity not found.
  • 409 – Duplicate external_id on creation.
  • 400 – Invalid request structure.
note

There are no other APIs that support external_id, for the APIs that require a Kratos identity_id you need to use the Get identity by external_id API above and use the identity id from there.

JWT configuration

Jsonnet example

When tokenizing sessions, external_id is available in the session context:

local claims = std.extVar('claims');
local session = std.extVar('session');

{
claims: {
iss: claims.iss + "/additional-component",
schema_id: session.identity.schema_id,
external_id: session.identity.external_id,
session: session,
}
}

Token behavior with external_id

If subject_source is set to external_id in the tokenizer template, the JWT's sub claim becomes:

{
"sub": "customer-12345"
}

If external_id is missing, tokenization will fail.

Migration guide

To migrate from an existing system, you can bulk import identities into Kratos and set their external_id using the Identity Import API.

Use PATCH /admin/identities

Basic import example

[
{
"schema_id": "default",
"external_id": "customer-001",
"traits": {
"email": "alice@example.com"
}
}
]

Pre-hashed password example

[
{
"schema_id": "default",
"external_id": "customer-002",
"traits": {
"email": "bob@example.com"
},
"credentials": {
"password": {
"config": {
"hashed_password": "$2b$12$abc123..." // bcrypt hash
}
}
}
}
]

Migration tips

  • Pre-hash passwords to avoid timeouts
  • Validate external_id uniqueness before import
  • Import in smaller batches (≤ 200 identities if using plaintext passwords)

Troubleshooting

Error / CodeContextDescription
409 ConflictMigration (batch import)One or more identities have a duplicate external_id. Ensure all values are unique.
400 Bad RequestMigration (batch import)The request payload is invalid or improperly formatted. Check JSON structure and required fields.
504 Gateway TimeoutMigration (batch import)The batch is too large or includes plaintext passwords. Reduce batch size or pre-hash passwords.
500 Internal Server ErrorSession token generationIf subject_source = external_id is configured, the session will not tokenize unless the identity has an external_id set.

For advanced examples, see: