Create a user

POST /v1/users · permission users:write

Creates a user in the environment of the API key. The user belongs to that environment only; the same email in two environments is two separate users. Unique values (email, phone_number, username, external_id) are checked per environment. Send an Idempotency-Key header to make retries safe.

Request

The body is a JSON object. Unknown fields are rejected with unknown_field.

Field Type Required Description
email string no* Primary email address.
email_verified boolean no Marks the email as verified. Default false.
phone_number string no* Primary phone number in E.164 format.
phone_verified boolean no Marks the phone number as verified. Default false.
phone_country_code string no ISO 3166-1 alpha-2 code. Derived from the phone prefix when empty.
username string no* 3 to 64 characters. Unique in the environment.
external_id string no Your own identifier, 1 to 255 characters. Unique in the environment.
name, given_name, family_name, middle_name, nickname string no Profile names.
picture, website, profile_url string no https:// URLs, up to 2048 characters.
locale, zoneinfo, gender, birthdate string no Profile attributes. birthdate is YYYY-MM-DD.
address object no OIDC address object, up to 2048 bytes.
user_metadata, app_metadata object no Free-form JSON objects, up to 16 KiB, depth 5, 100 top-level keys.

* At least one of email, phone_number or username is required.

Response

201 Created. The body is the user object. The ETag header carries the user version.

Personal data is masked when the key lacks the users:read_pii permission: email becomes a***@e***.com, phone_number keeps only its last 2 digits (+**********67), name, given_name, family_name, middle_name and nickname become initials (A. L.), username keeps only its first character (a***), birthdate becomes ****-**-** and address becomes {"redacted": true}. user_metadata and app_metadata are not masked.

{
  "id": "usr_0a1b2c3d4e5f6g7h8j9k0m1n2p",
  "kind": "regular",
  "status": "active",
  "external_id": null,
  "username": null,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "email_verified": true,
  "phone_number": null,
  "phone_verified": false,
  "user_metadata": {},
  "app_metadata": {},
  "created_via": "admin_api",
  "created_at": "2026-01-15T09:30:00Z",
  "updated_at": "2026-01-15T09:30:00Z",
  "last_sign_in_at": null,
  "deletion_requested_at": null,
  "deletion_due_at": null
}

Errors

Every error is an application/problem+json body with a stable code.

HTTP code When
400 malformed_request, unknown_field The body is not valid JSON or has a field that is not in the schema.
409 conflict The value is already in use. errors[] names the field and the reason (email_taken, phone_taken, username_taken, external_id_taken).
415 unsupported_media_type Content-Type is not application/json.
422 validation_failed A value is invalid; errors[].pointer names the field.
401 unauthenticated The API key is missing, invalid or revoked.
403 insufficient_permissions The key lacks the users:write permission.
429 rate_limited The rate limit was exceeded; honor Retry-After.
503 service_unavailable A dependency is temporarily unavailable; retry later.

Example

curl -X POST https://api.eu.keenauth.com/v1/users \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"email": "ada@example.com", "email_verified": true, "name": "Ada Lovelace"}'

Personal data

This endpoint carries personal data: end-user profile. Personal data in the KeenAuth APIs describes what is stored, how long it is kept and how it is deleted. Do not send special categories of personal data (health, biometric data, religion, political opinions, sexual orientation) in any field.