API Reference: Gate Identity

All Gate Identity endpoints are served from https://api.vernesoft.com.

Examples are shown as raw curl requests and as calls through the official SDKs — see Installation to set one up. SDK snippets assume an initialized verne client.

Every request requires a tenant API key in the Authorization header:

Authorization: Bearer vrn_9f8a7...

The key needs the gate scope. A key without it is refused with 403 Forbidden and the code forbidden (This API key is not authorized for Gate.).


Identity Management

A tenant API key can read and change only the identities of its own tenant. The Edge Gateway checks this on every request.

EndpointMethodsDescription
/v1/gate/identitiesPOSTCreate a new end-user identity
/v1/gate/identities/{id}GET, PATCH, DELETERead, update, or delete an identity
/v1/gate/identities/{id}/statePATCHActivate or deactivate an identity
/v1/gate/identities/{id}/metadataGET, PATCHRead or update custom metadata
/v1/gate/identities/{id}/sessionsGET, DELETEList or revoke all sessions for an identity
/v1/gate/sessions/{session_id}DELETERevoke a single session
/v1/gate/identities/{id}/resend-verificationPOSTResend verification email

Separate end users per tenant

End users are kept apart by tenant. The same email address can be an end user of two tenants, and they are two different people: each has their own password, their own sessions and their own verification and recovery mail, and a tenant's key sees only its own. Within one tenant, an address is one end user.

Your requests do not change for this. You send and read the address as the user typed it; creating, signing in, registering, recovering and changing an address work as described below.

Limits

  • A social account belongs to one tenant. One particular Google or GitHub identity can be an end user of only one tenant. When a second tenant's login ends with that account, the browser receives 403 forbidden with the message This sign-in cannot be used with this project. and no session is issued. A different social account with the same email address is a different person, and is allowed. See Social Auth.
  • Address length: at most 208 bytes. The limit applies to the address once its domain is written in punycode. A longer address is refused with 400 (see Create Identity).
  • No quoted part before the @. An address such as "a b"@example.com is refused with 400.
  • Not in the gate.invalid zone. gate.invalid and its subdomains are reserved; an address there is refused with 400.
  • Addresses are not case-sensitive. Alice@Example.com and alice@example.com are the same end user, and mail goes to the lower-case form. Capital letters in the part before the @ are not kept, so do not rely on getting them back.
  • Domains in punycode. A domain written in Unicode (user@münchen.de) is refused with 400; write it in punycode (user@xn--mnchen-3ya.de).

Create Identity

POST /v1/gate/identities

Creates a new end-user identity bound to your tenant.

Request body — Kratos-compatible identity object:

FieldTypeRequiredDescription
schema_idstringYesMust be "user"
traits.emailstringYesEnd-user email address — see Limits
credentials.password.config.passwordstringNoInitial password (omit for passwordless/social users)
statestringNo"active" (default) or "inactive"
curl -X POST https://api.vernesoft.com/v1/gate/identities \
  -H 'Authorization: Bearer vrn_9f8a7...' \
  -H 'Content-Type: application/json' \
  -d '{
    "schema_id": "user",
    "traits": { "email": "user@example.com" },
    "credentials": {
      "password": { "config": { "password": "StrongPassword123!" } }
    },
    "state": "active"
  }'

Returns the created identity object (201 Created), with traits.email and the verification and recovery addresses showing the address you sent. Fires the identity.created webhook event. Any verifiable_addresses or recovery_addresses in the request body are ignored; Gate sets them from traits.email.

Errors

StatusCodeWhen
409conflictAn identity with those traits already exists. You already have an end user with this address. The same address at another tenant does not count: it is a different person.
400invalid_payloadThe address cannot be used. The message says why (below).
400invalid_payloadA trait in the request is managed by the service and cannot be set. The request tried to set one of the two internal fields Gate keeps in an end user's traits to tell tenants' users apart. They are never shown in a response, and no request can set them.

The messages for an address that cannot be used:

Whymessage
Not an addressThat is not an email address this service can use.
Longer than 208 bytesThat email address is too long: at most 208 characters are accepted.
Quoted part before the @An email address with a quoted part before the @ cannot be used.
Part before the @ longer than 64 charactersThat email address is not valid: the part before the @ is longer than 64 characters.
In the gate.invalid zoneThat email address belongs to a reserved domain and cannot be used.

The body has the usual error shape: { "error": { "code": "invalid_payload", "message": "…", "request_id": "req_…" } }. A domain written in Unicode is also 400 invalid_payload. See Limits.

An account over its active-user limit, once the month to move to a higher plan has ended, is refused with 402 ACTIVE_USER_LIMIT_REACHED before anything is created — see Rate Limits & Plan Limits. A user created here is not an active user until they sign in.


Get Identity

GET /v1/gate/identities/{id}

Returns the full Kratos identity object. Includes traits, metadata_public, state, and credential metadata (no secrets).

curl https://api.vernesoft.com/v1/gate/identities/f47ac10b-... \
  -H 'Authorization: Bearer vrn_9f8a7...'

Update Identity

PATCH /v1/gate/identities/{id}

Updates traits or other mutable fields. Body is a JSON Patch (RFC 6902) — an array of operations applied to the identity object; fields not referenced by any operation are unchanged.

Changing the email address. A replace or add on /traits/email changes the address everywhere the end user is known by it: the next sign-in, verification mail and recovery mail all use the new one, and the old address no longer signs in. If you already have another end user with the new address the answer is 409 conflict; the same address at another tenant is no obstacle. An address that cannot be used is refused with 400 invalid_payload, with the messages listed under Create Identity.

What a patch cannot do. An operation on the two internal fields Gate keeps in an end user's traits, or a replacement of the whole /traits object that includes them, is refused with 403 forbidden and the message That trait cannot be set or changed through this endpoint. Nothing in the patch is applied.

curl -X PATCH https://api.vernesoft.com/v1/gate/identities/f47ac10b-... \
  -H 'Authorization: Bearer vrn_9f8a7...' \
  -H 'Content-Type: application/json' \
  -d '[
    { "op": "replace", "path": "/traits/email", "value": "newemail@example.com" }
  ]'

Delete Identity

DELETE /v1/gate/identities/{id}

Permanently deletes the identity. Returns 204 No Content. Fires the identity.deleted webhook event.

curl -X DELETE https://api.vernesoft.com/v1/gate/identities/f47ac10b-... \
  -H 'Authorization: Bearer vrn_9f8a7...'

Activate / Deactivate Identity

PATCH /v1/gate/identities/{id}/state

Changes the identity state. The identity is not deleted: set it back to active and the person is back as they were.

FieldTypeValues
statestring"active" or "inactive"

While an identity is inactive:

  • Sign-in is refused by every method — password, one-time code, social login and account recovery. The person is told This account was disabled. and no session is issued.
  • Sessions they already hold stop working. GET /v1/gate/auth/session answers 401 for them. The sessions are suspended, not revoked: once the identity is active again, the same session tokens work again. To end a session for good, revoke it.
  • Their Gate access tokens are inactive. POST /v1/gate/tokens/introspect answers { "active": false } for a token whose subject is the identity, and POST /v1/gate/authorize answers allowed: false. The same token is active again after reactivation.

Setting the state an identity is already in succeeds and changes nothing. On success the response is the identity as it now is. An identity that is not yours is refused with 403.

curl -X PATCH https://api.vernesoft.com/v1/gate/identities/f47ac10b-.../state \
  -H 'Authorization: Bearer vrn_9f8a7...' \
  -H 'Content-Type: application/json' \
  -d '{ "state": "inactive" }'

Fires the identity.state_changed webhook event on success. See Custom Metadata for details.


Custom Metadata

GET  /v1/gate/identities/{id}/metadata
PATCH /v1/gate/identities/{id}/metadata

Read or merge-update the metadata_public JSON object attached to an identity. Pass null for a key to delete it.

See Custom Metadata for full documentation.


Session Management

GET    /v1/gate/identities/{id}/sessions
DELETE /v1/gate/identities/{id}/sessions
DELETE /v1/gate/sessions/{session_id}

List or revoke active sessions for an end-user. Tenant isolation is enforced — you can only manage sessions of identities that belong to your tenant.

See Session Management for the full reference including response format and integration examples.


Resend Verification Email

POST /v1/gate/identities/{id}/resend-verification

Triggers a new verification email for an unverified identity. The link is valid for the flow TTL configured in Kratos.

curl -X POST https://api.vernesoft.com/v1/gate/identities/f47ac10b-.../resend-verification \
  -H 'Authorization: Bearer vrn_9f8a7...'

Auth Flows

Self-service flows for end-users. Login, registration, recovery, verification and settings each start a Kratos API flow and return the flow object, whose ui.nodes your frontend renders. See Settings Flow for the full flow lifecycle.

EndpointMethodDescription
/v1/gate/auth/loginGETInitiate login flow. Optional return_to and state for social login
/v1/gate/auth/registrationGETInitiate registration flow — see Registration. Same optional return_to and state
/v1/gate/auth/recoveryGETInitiate account recovery flow
/v1/gate/auth/recovery/submitPOSTSubmit recovery email or code
/v1/gate/auth/verificationGETInitiate email verification flow
/v1/gate/auth/verification/submitPOSTSubmit verification email or code
/v1/gate/auth/settingsGETInitiate settings flow (requires X-Session-Token)
/v1/gate/auth/settings/submitPOSTSubmit settings change (requires X-Session-Token)
/v1/gate/auth/oidc/exchangePOSTTrade the one-time code from a social login for the session — see Social Auth

Every endpoint that takes an X-Session-Token — GET /v1/gate/auth/session, GET /v1/gate/auth/login?aal=aal2, GET /v1/gate/auth/settings and POST /v1/gate/auth/settings/submit — checks that the session belongs to one of your tenant's end users before it does anything with it. A token that is expired, another tenant's user's, or a dashboard account's is answered 401 with "code": "unauthorized" and "message": "That session is not active." — identically for all of them, so the answer never confirms that a token is real. The one exception is for your own user: the first-factor session of a person who has enrolled an authenticator answers 403 mfa_required on GET /v1/gate/auth/session (and starts the step-up) for your key only; any other key gets the 401 (MFA / TOTP). A missing header is 400; if Kratos cannot be asked, 502 upstream_unavailable.

Completing a login

Login has no .../submit counterpart: it is completed by a plain POST to the ui.action URL carried by the flow itself — a Kratos public route under /auth/*. Send JSON, with no API key, no cookie and no CSRF token; the flow id in the URL is the whole binding.

# 1. Your backend starts the flow with your Gate key.
curl https://api.vernesoft.com/v1/gate/auth/login \
  -H 'Authorization: Bearer vrn_9f8a7...'
# → { "id": "…", "type": "api", "ui": { "action": "https://api.vernesoft.com/auth/self-service/login?flow=…", … } }

# 2. The user's credentials go to that action URL.
curl -X POST 'https://api.vernesoft.com/auth/self-service/login?flow=…' \
  -H 'Content-Type: application/json' \
  -d '{ "method": "password", "identifier": "user@example.com", "password": "…" }'
# → 200 { "session_token": "ory_st_…", "session": { … } }

The session_token in that answer is what the settings flow and the second factor expect as X-Session-Token. Keep it on your server. Wrong credentials answer 400 with the flow object and its ui.messages, ready to render again. See Settings Flow and MFA / TOTP for the exchange that follows.

A password, code or registration login is an API flow with no browser in the loop, so there is no redirect after it: your application decides where the user goes, and the X-Return-To header is not read. The one place a flow ends in a browser is a social login, which has its own, optional return — a project registers return URLs and the browser comes back to one of them with a one-time code. That is described in Social Auth, and a project that has registered none is not affected.

Sign in with a one-time code

A user can sign in with a six-digit code mailed to them instead of a password. It is off until you switch it on for your tenant: set passwordless_enabled to true in Security Settings. With it on, the login flow carries a node with group: "code"; with it off there is none, and a method: "code" submission is refused (see below).

The flow is the login flow above, in two submissions to the same ui.action — first the address, then the code. As with a password login, send JSON with no API key, no cookie and no CSRF token.

# 1. Your backend starts the flow with your Gate key.
curl https://api.vernesoft.com/v1/gate/auth/login \
  -H 'Authorization: Bearer vrn_9f8a7...'
# → { "id": "<flow>", "type": "api", "ui": { "action": "https://api.vernesoft.com/auth/self-service/login?flow=<flow>", … } }

# 2. The address. Kratos mails the user a six-digit code, valid for 15 minutes.
curl -X POST 'https://api.vernesoft.com/auth/self-service/login?flow=<flow>' \
  -H 'Content-Type: application/json' \
  -d '{ "method": "code", "identifier": "user@example.com" }'
# → 400 { "state": "sent_email", "ui": { "messages": [ { "id": 1010014, … } ], … }, … }

# 3. The code the user read in the mail — to the same URL, with the address again.
curl -X POST 'https://api.vernesoft.com/auth/self-service/login?flow=<flow>' \
  -H 'Content-Type: application/json' \
  -d '{ "method": "code", "identifier": "user@example.com", "code": "123456" }'
# → 200 { "session_token": "ory_st_…", "session": { … } }

The answer to step 3 is the same as a password login's: session_token is what the settings flow and the second factor expect as X-Session-Token, and /v1/gate/auth/session reads it. Keep it on your server.

Things your integration has to know:

  • Step 2 answers 400, not 200. That is how a flow that is waiting for the next field answers; "state": "sent_email" is the success. The same flow object comes back with the code input (ui.nodes, group: "code") ready to render.
  • Only your own end users are mailed. For an address that is not one of your tenant's end users — nobody, a Verne account owner, a person who is an end user of another tenant only — step 2 answers 400 with "state": "choose_method" and message 4000035 (This account does not exist or has not setup sign in with code) and no mail is sent. It is the same answer for all three, so it does not say whose address it is. If the same person is an end user of your tenant too, they are mailed as your end user. For your own end users it does say whether the address has an account: render it as you see fit.
  • A wrong code answers 400, "state": "sent_email" and message 4010008; the right code still works. After five wrong submissions the flow refuses even the right one (message 4000001, submitted too often) — start a new flow and ask for a new code.
  • A user with no password signs in this way too. POST /v1/gate/identities without credentials creates exactly that user, and it is the only way they can sign in.
  • Your tenant's switch is enforced, not just hidden. With passwordless_enabled off, posting { "method": "code" } yourself is answered like any method that does not exist: 400, message 4010002 (Could not find a strategy to log you in with), and nothing is mailed. The same holds if the setting cannot be read at that moment: the method is treated as off.
  • The session is your tenant's. A person who is an end user of another tenant only, and types their address into your flow, is not mailed (above). The code opens your tenant's end user with that address and no one else's, and if a login ever ended in someone else's person it is refused exactly as a wrong password is.

Recovery and verification, below, use the same code method and are not switched by passwordless_enabled.

Recovery and verification

Both are submitted through the gateway, in two phases each: first { "method": "code", "email": "…" }, which sends the user a code, then { "method": "code", "code": "…" }. A verification that succeeds answers "state": "passed_challenge".

A recovery that succeeds answers "state": "passed_challenge" too, and hands you a session for the recovered user — which is what lets your backend carry them on to a new password. The session is not a top-level session_token, as it is for a login: it is an item of the continue_with list.

# 1. Your backend starts the flow with your Gate key.
curl https://api.vernesoft.com/v1/gate/auth/recovery \
  -H 'Authorization: Bearer vrn_9f8a7...'
# → { "id": "<flow>", "type": "api", "ui": { … } }

# 2. The address. Kratos mails the user a six-digit code.
curl -X POST 'https://api.vernesoft.com/v1/gate/auth/recovery/submit?flow=<flow>' \
  -H 'Authorization: Bearer vrn_9f8a7...' \
  -H 'Content-Type: application/json' \
  -d '{ "method": "code", "email": "user@example.com" }'
# → 200 { "state": "sent_email", … }

# 3. The code the user read in the mail.
curl -X POST 'https://api.vernesoft.com/v1/gate/auth/recovery/submit?flow=<flow>' \
  -H 'Authorization: Bearer vrn_9f8a7...' \
  -H 'Content-Type: application/json' \
  -d '{ "method": "code", "code": "123456" }'
# → 200 { "state": "passed_challenge",
#         "continue_with": [ { "action": "set_ory_session_token", "ory_session_token": "ory_st_…" }, … ], … }

# 4. That token opens the settings flow — start it, then set the password.
curl https://api.vernesoft.com/v1/gate/auth/settings \
  -H 'Authorization: Bearer vrn_9f8a7...' \
  -H 'X-Session-Token: ory_st_…'
# → { "id": "<settings flow>", … }
curl -X POST 'https://api.vernesoft.com/v1/gate/auth/settings/submit?flow=<settings flow>' \
  -H 'Authorization: Bearer vrn_9f8a7...' \
  -H 'X-Session-Token: ory_st_…' \
  -H 'Content-Type: application/json' \
  -d '{ "method": "password", "password": "…" }'
# → 200 { "state": "success", … }

Take the token from the continue_with item whose action is set_ory_session_token (ory_session_token) and pass it as X-Session-Token, exactly as you would a login's session_token; keep it on your server. A wrong code is not an error status: the answer is 200 with "state": "sent_email" and the refusal in ui.messages, ready to render again, and it does not use up the right code. The show_settings_ui item in the same list points at the settings screen of the Verne dashboard; an API flow has no use for it, so ignore it.

Recovery and verification mail only your own end users: an address that is not one of your tenant's end users — an unknown address, a Verne account owner, a person who is an end user of another tenant only — gets the same 200 / "state": "sent_email" answer as any other and no mail is sent. The answer deliberately does not say which, so these endpoints cannot be used to find out who has an account.

If the same address is an end user of two tenants, each tenant's flow reaches only its own end user: a recovery code started with your key goes to the real address, and the new password or the verification applies to your end user alone. The other tenant's end user is not touched.

The recovered session is for whoever owns the address — the code only ever goes to that mailbox — so treat a recovery like a login: it proves the user controls the address, nothing more.

Registration

Registration works the way login does: your backend starts the flow with your Gate key, and the user's details go to the ui.action URL the flow carries — JSON, with no API key, no cookie and no CSRF token. It takes two submissions to that same URL: first the traits (method: "profile"), then the password (method: "password", with the traits again).

# 1. Your backend starts the flow with your Gate key.
curl https://api.vernesoft.com/v1/gate/auth/registration \
  -H 'Authorization: Bearer vrn_9f8a7...'
# → { "id": "…", "type": "api", "ui": { "action": "https://api.vernesoft.com/auth/self-service/registration?flow=…", … } }

# 2. The traits. Kratos answers 400 with the same flow, now asking for a password —
#    that is "accepted, continue", not a refusal.
curl -X POST 'https://api.vernesoft.com/auth/self-service/registration?flow=…' \
  -H 'Content-Type: application/json' \
  -d '{ "method": "profile", "traits.email": "user@example.com", "traits.locale": "en" }'

# 3. The password.
curl -X POST 'https://api.vernesoft.com/auth/self-service/registration?flow=…' \
  -H 'Content-Type: application/json' \
  -d '{ "method": "password", "traits.email": "user@example.com", "traits.locale": "en", "password": "…" }'
# → 200 { "session_token": "ory_st_…", "session": { … }, "identity": { "id": "…", … } }

The answer is the same shape as a login's: session_token is what the settings flow and the second factor expect as X-Session-Token, and the user is logged in from that moment. A rejected password (too short, found in a known breach) answers 400 with the flow and the reason in its ui messages, ready to render again.

The person who registers is your tenant's end user, exactly as if you had created them with POST /v1/gate/identities: they are yours to read, update and delete with your key, they appear in the Users list of your dashboard, and they are recorded in the project the key belongs to. They are not a Verne account — registering creates nothing on the billing side — and an identity.created webhook event with "method": "password" is sent if you subscribed to it. If the user later logs in through a key of another project of yours, they are recorded in that project too.

An account over its active-user limit, once the month to move to a higher plan has ended, cannot take new registrations: GET /v1/gate/auth/registration answers 402 ACTIVE_USER_LIMIT_REACHED instead of a flow (see Rate Limits & Plan Limits), and a flow that was already open is refused at its last step with the same message. Signing in is never affected.

The flow has to be started with your Gate key. A registration flow started any other way — straight on /auth/self-service/registration/api, for example — has no tenant to attribute the new user to, and the registration is refused at the password step instead of creating a user nobody owns.

The traits can be written two ways: nested, { "traits": { "email": "user@example.com" } }, or flat, { "traits.email": "user@example.com" } as in the examples above. Both are accepted. Use one spelling per request; a body that spells the address both ways is not supported.

If one of your end users already has the address, the password step fails with message 4000007 (An account with the same identifier … exists already), as for a registration by code below. The same address as an end user of another tenant does not matter. An address that cannot be used (see Limits) is refused with 400 and message 4000010 in the flow's ui.messages, carrying the same text as in Create Identity.

Like every API flow it does not redirect, and an X-Return-To header is not read. Social registration is a separate path; see Social Authentication.

Registration by one-time code

With passwordless_enabled on, a user can register without choosing a password. The flow offers the code method next to the password (a node with group: "code" in the 400 that answers the traits step). Submit method: "code" with the traits, then again with the code from the mail; the traits step on its own is optional here — it only shows which methods are offered, and method: "code" with the traits works straight away:

# 2. Optional: the traits alone — the answer lists the methods the user can continue with.
curl -X POST 'https://api.vernesoft.com/auth/self-service/registration?flow=…' \
  -H 'Content-Type: application/json' \
  -d '{ "method": "profile", "traits.email": "user@example.com", "traits.locale": "en" }'

# 3. Ask for the code. Kratos mails it to the address.
curl -X POST 'https://api.vernesoft.com/auth/self-service/registration?flow=…' \
  -H 'Content-Type: application/json' \
  -d '{ "method": "code", "traits.email": "user@example.com", "traits.locale": "en" }'
# → 400 { "state": "sent_email", … }

# 4. The code, with the traits again.
curl -X POST 'https://api.vernesoft.com/auth/self-service/registration?flow=…' \
  -H 'Content-Type: application/json' \
  -d '{ "method": "code", "traits.email": "user@example.com", "traits.locale": "en", "code": "123456" }'
# → 200 { "session_token": "ory_st_…", "session": { … }, "identity": { "id": "…", … } }

The person is your tenant's end user exactly as after a password registration, and the identity.created event reports "method": "code". Because the code went to the address, it is also what proves the address: it is marked verified. With the setting off, a method: "code" registration is refused (400, message 4010003) and nobody is created. If one of your end users already has the address, step 4 fails with 4000007 (An account with the same identifier … exists already) — registration never signs anybody in to an existing account.


Tenant Settings

OIDC Providers

GET  /v1/gate/settings/oidc-providers
PUT  /v1/gate/settings/oidc-providers

Read or replace the list of social login providers enabled for your tenant. See Social Auth for configuration details.


Return URLs

GET /v1/gate/settings/return-urls
PUT /v1/gate/settings/return-urls

The addresses on your backend that receive an end user after a social login — per project, matched exactly. PUT takes { "return_urls": ["https://…"] }, replaces the list and answers with it; the first entry is the default. See Return URLs for what is accepted.


Security Settings

GET /v1/gate/settings/security
PUT /v1/gate/settings/security

Read or update per-tenant security feature flags.

FieldTypeDescription
passwordless_enabledbooleanEnable sign-in and sign-up by an emailed one-time code — see Sign in with a one-time code
mfa_enabledbooleanEnable TOTP two-factor authentication
curl -X PUT https://api.vernesoft.com/v1/gate/settings/security \
  -H 'Authorization: Bearer vrn_9f8a7...' \
  -H 'Content-Type: application/json' \
  -d '{ "passwordless_enabled": true, "mfa_enabled": false }'

Webhooks

GET    /v1/gate/settings/webhooks
POST   /v1/gate/settings/webhooks
PATCH  /v1/gate/settings/webhooks/{id}
DELETE /v1/gate/settings/webhooks/{id}

Manage webhook endpoints that receive real-time identity event notifications. See Webhooks for the full reference including payload format and signature verification.


Tokens & Authorization

See Access & Authorization for the full token lifecycle reference.

EndpointMethodDescription
/v1/gate/tokensPOSTCreate a short-lived Gate access token
/v1/gate/tokens/introspectPOSTValidate and decode a Gate token
/v1/gate/authorizePOSTCheck if a subject is allowed to perform an action

Rate Limits & Plan Limits

Identity management calls are not metered: Nautilus has no request quota, and no plan limits how often you call the API. What a plan limits is active users — the people who sign in to your application at least once in a calendar month — and what happens past that limit is described on Plans, Limits and Refusals.

There is a burst ceiling of 60 requests per minute per tenant on the identity management endpoints. It exists to protect the identity store from a runaway loop, not to ration your plan: sixty a minute is an identity created every second, sustained. It is the same on every plan.

The refusals you can meet are distinguishable, and each tells you what to do next:

ResponseMeaningWhat to do
429 with "limit": 60The per-minute burst ceiling.Back off for the Retry-After window — a second at most.
402 with "code": "ACTIVE_USER_LIMIT_REACHED"Your account is over its active-user limit and the month to move to a higher plan has ended, so new end users cannot be created. Signing in is never refused.Move to a higher plan; retrying does not help. See below.
402 with BILLING_HARD_BLOCKED or SUBSCRIPTION_PAST_DUEThe account is suspended for billing, or a renewal is unpaid. This is for calls to the Admin API; signing in is never refused for it.Settle the balance; retrying does not help.

A 429 carries a Retry-After header giving the seconds until the limit lifts. Bulk imports should pace themselves against it rather than guessing.

Active-user limit: 402 ACTIVE_USER_LIMIT_REACHED

PlanActive users a month
Free1,000
Pro10,000
Advanced50,000

An end user is active in a month once they have signed in, by any method, however often. A user you create with POST /v1/gate/identities and who never signs in is not counted.

Past the limit, sign-in is never blocked. You are emailed at 80 % and at 100 % of the limit. After you pass it you have one full calendar month to move to a higher plan; after that month, if you are still over, only new registrations are refused. Anyone who already has an account keeps signing in.

What is refused is the creation of a new end user — POST /v1/gate/identities, starting a registration with GET /v1/gate/auth/registration, registering through a flow already open, a first social sign-in — with one response everywhere:

{
  "error": {
    "code": "ACTIVE_USER_LIMIT_REACHED",
    "message": "This account is over its monthly active-user limit and the grace month to move up has ended, so new end users cannot be created. People who already have an account can still sign in. Move to a higher plan in the dashboard, or write to support, to lift it.",
    "request_id": "req_…",
    "plan": "free",
    "limit": 1000,
    "current": 1043,
    "sign_in": "unaffected"
  }
}

It is a 402 and has no Retry-After: it lifts when you move to a higher plan, not with time. Reading, updating and deleting users, recovery, verification and every kind of sign-in are never refused for this reason. The exact timeline, and where else the refusal appears, is on Plans, Limits and Refusals.


Error Format

All errors use the same envelope:

{
  "error": "description of what went wrong"
}

Kratos flow errors return the full flow object with a populated ui.messages array — render these to guide the end-user.

HTTP StatusMeaning
400Bad request — invalid body or unsupported field value
403Forbidden — API key lacks the required scope, or identity belongs to a different tenant
404Not found — identity or webhook does not exist
422Unprocessable — Kratos flow validation failed (check ui.messages)
402Payment required — ACTIVE_USER_LIMIT_REACHED (a new end user, past the active-user limit), or the account is suspended for billing or a renewal is unpaid
429Too many requests — the burst ceiling; retry after Retry-After
500Internal server error — contact support with the request timestamp