Verify MFA

POST /v1/auth/mfa

Second step of an MFA-protected login. Call this only after /auth/login returns mfa_required: true: send the mfa_challenge_token from that response plus the code from the user's authenticator app, and you get the same session token /auth/login returns for an account without MFA. The SSO callback issues the same kind of challenge when a user who signed in through their identity provider also has MFA on.

The challenge is valid for 5 minutes and is used up by a successful verification; a wrong code doesn't use it up, so the user can try again. Older clients may instead re-send username + password with the code; that form still works and counts against the sign-in attempt limit.

Attempt limit: each user gets 5 code checks per sliding hour, right or wrong. The limit is shared with the other MFA code checks: /mfa/activate, /mfa/verify, /mfa/disable, /mfa/backup-codes/regenerate and step-up re-authentication. Past it the API answers 429 with the minutes until the oldest check leaves the window.

Request body

mfa_challenge_token string
The challenge from the /auth/login (or SSO callback) response. Required unless you send username + password.
token string Req
The six-digit code from the user's authenticator app, or one of the user's MFA backup codes. A code can't be replayed: an authenticator code is accepted once, and a backup code is removed when it's used.
username, password string
Legacy alternative to mfa_challenge_token: the same credentials as the /auth/login call, re-verified on this request.

Unknown fields are rejected with 400 (strict decoding).

Response (200 OK)

success boolean
Always true on a successful verification.
access_token string
Signed JWT — same shape as /auth/login. Send it as Authorization: Bearer <token>.
expires_at, expires_in datetime, int
When the session token expires (RFC 3339, UTC) and the seconds left until then.
user object
Minimal user object: uuid, username, email, role. (It also carries the legacy integer id until the sunset; don't use it.)

Errors

Errors are JSON: {"error": "<message>", "code": "<CODE>"}.

400 Bad Request
No token ("Token is required"), neither a challenge nor username + password, unknown fields, or MFA_NOT_ENABLED (the user has no MFA set up).
401 Unauthorized
MFA_INVALID_TOKEN — wrong or already-used code ("Invalid authentication code. Please try again."). MFA_CHALLENGE_INVALID — the challenge is unknown, used or older than 5 minutes ("Your sign-in session expired. Please sign in again."); sign in again. INVALID_CREDENTIALS — legacy form only, wrong username or password.
403 Forbidden
EMAIL_NOT_VERIFIED — the user hasn't verified their email; the body also carries user_uuid and a one-time resend_token, as on /auth/login. SSO_REQUIRED — the user's account enforces SSO, so a password sign-in (legacy form or a password challenge) is refused.
429 Too Many Requests
MFA_RATE_LIMITED — the user used their 5 code checks for the hour ("Too many attempts. Try again in N minutes."). The legacy form can also hit the sign-in attempt limit, with a Retry-After header.
503 Service Unavailable
SSO_CHECK_UNAVAILABLE — the SSO enforcement check couldn't run; retry.
Request
curl -X POST "https://api.ngris.com/v1/auth/mfa" \ -H "Content-Type: application/json" \ -d '{ "mfa_challenge_token": "<mfa_challenge_token from /auth/login>", "token": "654321" }'
Python (the SDK sends the legacy username + password form)
from ngris import Ngris client = Ngris(api_key="placeholder") resp = client.auth.mfa_verify( username="maria", password="<password>", token="654321", ) print(resp.access_token)
Response — 200 OK
{ "success": true, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJtYXJpYSJ9.c2lnbmF0dXJl", "expires_at": "2026-10-21T09:00:00Z", "expires_in": 86400, "user": { "uuid": "019a52c2-ca1e-78ba-ab5c-50362cc29134", "username": "maria", "email": "maria@example.com", "role": "user" } }
Response — 401 wrong code
{ "error": "Invalid authentication code. Please try again.", "code": "MFA_INVALID_TOKEN" }
Response — 429 attempt limit
{ "error": "Too many attempts. Try again in 42 minutes.", "code": "MFA_RATE_LIMITED" }
Was this helpful?
On this page
Verify MFA
Was this helpful?