---
title: "Login as a local user"
method: POST
path: "/login"
tags: ["Auth"]
---

# Login as a local user

`POST /login`

## Request

- Base URL: `http://localhost:8000`
- URL: `http://localhost:8000/login`
- Auth: none declared

Verify the user's email + password. Returns one of two shapes:

- ``kind: "session"`` — the user has no active MFA. The body
  carries a full session JWT (``mfa_satisfied=false``). Backward-
  compatible with the pre-MFA shape; existing clients can ignore
  ``kind``.
- ``kind: "mfa_required"`` — the user has at least one active MFA
  method. The body carries a 5-minute ``mfa_challenge_token``
  which must be exchanged at ``/auth/mfa/verify-login`` for a real
  session JWT.

See docs/auth/USER_LOGIN.md and the MFA roadmap "Two-step login"
section.

## Headers

- `API-Version` '1'

## Response `200`

Successful Response

- union
  - LoginSuccessResponse: Returned by /login when the user has no active MFA. Backward-compat with the pre-MFA shape — extra ``kind`` field is the only diff.
    - `id` integer, required
    - `username` string, email, required
    - `firstname` string, nullable
    - `lastname` string, nullable
    - `displayname` string, nullable
    - `phone` string, nullable
    - `register_date` string, date-time, required
    - `roles` unknown[]
      - unknown
    - `profile_pic` string, required
    - `access_token` string, nullable
    - `pending_email` string, nullable
    - `mfa_satisfied` boolean
    - `mfa_enrolled` boolean
    - `mfa_recovery` boolean
    - `mfa_access_required` boolean
    - `kind` 'session'
    - `return_url` string, nullable
  - MfaChallengeResponse: Returned by /login when the user has at least one active MFA method. The challenge token must be exchanged at /auth/mfa/verify-login. ``mfa_methods`` is computed from the user's actual active method types (plus the implicit ``recovery_code`` fallback) so the client only renders branches the user can actually satisfy. ``preferred_hint`` is the most recently used primary factor (``totp`` / ``webauthn``) when both are enrolled — soft UI ordering only; ``None`` when neither has been used yet. ``extranet_handoff`` is true when the account holds ``admin`` or any ``sa_*`` role. NZ guest hosts use this to send operators to the AU-canonical login before starting an MFA ceremony (passkeys are RP-bound to ``totalparks.com.au``). A boolean avoids leaking the full role list on the unauthenticated challenge response.
    - `kind` 'mfa_required'
    - `mfa_challenge_token` string, required
    - `mfa_methods` string[], required
    - `preferred_hint` 'totp' | 'webauthn', nullable
    - `extranet_handoff` boolean
    - `return_url` string, nullable

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/totalparks/apis/total-parks-public-api.md) · [All operations](https://skmtc.dev/totalparks/apis/total-parks-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/totalparks/apis/total-parks-public-api/revisions/dcb4c822e2c6?raw)
