Login
Exchange an email (or username) and password for a JWT. The identifier is email-first: pass either your email address or your username in the username field — the server matches both and prefers an exact email match. The JWT is then sent in the Authorization: Bearer <token> header for subsequent requests; alternatively, mint an API key for long-running automation.
If the user has MFA enabled, this endpoint returns {"mfa_required": true, "mfa_challenge_token": "…"} with HTTP 200 instead of a JWT — no token is issued. Your client then calls POST /v1/auth/mfa with that mfa_challenge_token plus the TOTP code (the challenge is short-lived — see mfa_expires_in).
Sign-in limits. Attempts are counted per login name + client IP and per client IP: too many within 5 minutes lock that pair, or that IP, for 15 minutes. Across IPs a login name is only slowed down, never locked: once it has had too many wrong passwords, each further attempt from an IP must first wait a delay that doubles up to 30 seconds. Every refusal is a 429 with Retry-After. A successful sign-in sets a known-device cookie, __Host-ngris_login_device, valid 30 days from that sign-in: a later attempt that carries it for the same user skips the per-IP limit and the delay (the name + IP limit always applies). The dashboard keeps this cookie in your browser; a script calling this endpoint gets the normal limits unless it stores the cookie and sends it back. Changing or resetting the password ends the exemption for every browser; the next successful sign-in issues a new cookie.
Email verification is enforced — accounts with email_verified: false get 403 with code: "EMAIL_NOT_VERIFIED". If the user's active account enforces SSO for their email domain (via a domain-verified, enforced SSO config), password login is refused before the password is checked with 403 code: "SSO_REQUIRED" — the body carries sso_required, sso_provider, email_domain, and init_url so the client can redirect the user to their identity provider.
MFA still applies after SSO. If a user signs in through their identity provider but also has MFA enabled, they must complete the TOTP second factor before a session is minted — the SSO callback issues its own mfa_challenge_token for POST /v1/auth/mfa.
Request body
username for historical reasons but accepts either value.)Response — success (200 OK)
Authorization: Bearer <token> on subsequent requests. It is valid until expires_at. The token is scoped to one active account (carried in its claims); to act in a different account, mint a new token via POST /v1/user/switch-account. Don't decode the token to learn the account — read its uuid from GET /v1/account.uuid, username, email, email_verified, pending_email (an email change awaiting confirmation; omitted otherwise), role, status, mfa_enabled, created_at, updated_at, the active account (active_account_uuid and active_account_name, each omitted when unknown) and its plan slug. It also carries that plan's limit fields (max_tunnels, max_endpoints, max_connections, bandwidth_limit, request_limit, allowed_protocols, allowed_regions, allow_organizations), left out of the example — read the account's limits from GET /v1/account/plan — and balance_cents, which the sign-in doesn't load: it is always 0 here. Nothing else: read the full profile (last_login_at included) from GET /v1/user.Deprecated: until the sunset the user object also carries the legacy integer id and *_id fields. Don't use them.
Response — MFA required (200 OK)
mfa_challenge_token expires (currently 300 = 5 minutes). After that the client must log in again.uuid, username, email, role) for UI hints — do not trust as authenticated until MFA succeeds. (It also carries the legacy integer id until the sunset; don't use it.)Errors
username or password, or any unknown field (e.g. email or mfa_code — pass an email address in the username field, and send the TOTP code to /auth/mfa instead; the handler uses strict decoding).code: "EMAIL_NOT_VERIFIED") — direct user to /auth/resend-verification. Or the user's active account enforces SSO for their email domain (code: "SSO_REQUIRED", with sso_required, sso_provider, email_domain, and init_url set) — send the user to init_url to start SSO.Retry-After seconds before retrying.code: "SSO_CHECK_UNAVAILABLE"). Transient — retry shortly.