Skip to content

Auth API

The auth API is the imperative side of IntelliAuth's sign-in surface — the endpoints that take credentials in, validate them, and either return a session or kick off MFA. Most apps drive these via the React SDK and never touch them directly; this topic is the reference for non-SDK consumers.

POST /api/v1/auth/login
Content-Type: application/json
{
"email": "user@cymmetri.com",
"password": "..."
}

Three possible response shapes:

Success without MFA:

{
"data": {
"state": "success",
"session": { "id": "ses_01HZX...", "user_id": "usr_01HZX..." },
"access_token": "...",
"refresh_token": "...",
"expires_in": 3600
}
}

MFA required:

{
"data": {
"state": "mfa_required",
"flow_id": "flow_01HZX...",
"available_factors": ["webauthn", "totp", "sms"]
}
}

Failed:

{
"error": "invalid_credentials",
"message": "Email or password is incorrect.",
"request_id": "req_01HZX..."
}

The success-without-MFA shape includes tokens directly; this endpoint is the password-only entry point. For OAuth flows (PKCE, etc.), users do not call this — they go through /oauth2/authorize.

POST /api/v1/auth/logout
Authorization: Bearer <access-token>

Returns 204 No Content on success. Revokes the current session. To revoke other sessions too, hit the per-session revoke endpoint instead (see Me API).

GET /api/v1/auth/mfa/factors?flow_id=flow_01HZX...

When a flow returns mfa_required, fetch the factors available to this specific user:

{
"data": [
{ "id": "factor_01HZX...", "kind": "webauthn", "label": "MacBook Pro" },
{ "id": "factor_01HZY...", "kind": "totp", "label": "1Password" }
]
}

Show these to the user. They pick one.

For factors that require a server-side initiation (SMS sends a code, WebAuthn returns a challenge):

POST /api/v1/auth/mfa/begin
Content-Type: application/json
{
"flow_id": "flow_01HZX...",
"factor_id": "factor_01HZX..."
}

Response varies by factor kind:

{ "data": { "kind": "totp" } }
{
"data": {
"kind": "webauthn",
"challenge": "<base64url-bytes>",
"allow_credentials": [ ... ]
}
}
{ "data": { "kind": "sms", "sent_to_last4": "0100" } }
POST /api/v1/auth/mfa/complete
Content-Type: application/json
{
"flow_id": "flow_01HZX...",
"factor_id": "factor_01HZX...",
"code": "123456"
}

For WebAuthn, the body carries the assertion instead of code:

{
"flow_id": "flow_01HZX...",
"factor_id": "factor_01HZX...",
"assertion": { "id": "...", "rawId": "...", "response": { ... }, "type": "public-key" }
}

Success response carries tokens, identical to a non-MFA sign-in.

WebAuthn primary authentication (passwordless)

Section titled “WebAuthn primary authentication (passwordless)”
POST /api/v1/auth/webauthn/begin-authentication
Content-Type: application/json
{}

Response:

{
"data": {
"challenge": "<base64url-bytes>",
"rp_id": "banking-cymmetri.intelliauth.local",
"user_verification": "required"
}
}

The browser calls navigator.credentials.get() with the challenge, then:

POST /api/v1/auth/webauthn/finish-authentication
Content-Type: application/json
{
"assertion": { ... }
}

Success returns a session and tokens.

POST /api/v1/auth/step-up/initiate
Authorization: Bearer <current-access-token>
Content-Type: application/json
{
"required_acr": "aal2"
}

Response indicates which factor to complete:

{
"data": {
"flow_id": "step_01HZX...",
"factor": { "kind": "webauthn", "challenge": "..." }
}
}

Complete via the MFA-complete endpoint with the returned flow_id. The response carries a new access token whose acr claim reflects the new AAL.

ErrorWhen
invalid_credentialsEmail or password wrong
account_disabledThe user record is disabled
email_unverifiedThe user hasn't verified their email and tenant policy requires verification
mfa_factor_lockedToo many failed attempts; cool-down active
flow_expiredThe MFA flow_id is older than the policy window
flow_invalidThe flow_id doesn't exist or doesn't belong to this caller

Every error response has request_id. Log it.