---
title: "Complete sign-in with an MFA code."
method: POST
path: "/api/sign-in/mfa"
tags: ["Sign-in"]
---

# Complete sign-in with an MFA code.

`POST /api/sign-in/mfa`

## Request

- Base URL: `/` (relative, and the document was published on a file host, not at the API’s origin)
- URL: `/api/sign-in/mfa`
- Auth: none required

Takes the challenge POST /api/sign-in returned and a code from the user's authenticator app, and returns the session key. The challenge is used up by any attempt, right or wrong, so a wrong code means signing in again. Each code is accepted once. The fifth consecutive bad code locks the user until an administrator unlocks them. Requires no API key. Returns 404 unless PASSWORD_SIGN_IN_ENABLED is true. Recorded as sign_in_succeeded or sign_in_failed.

## Request body

- SignInMfaRequest
  - `challenge` string
  - `code` string

## Response `200`

Signed in. The session key is returned here and nowhere else.

- SignInResponse
  - `apiKey` string, required: The session key. Send it as a bearer token. It is returned here and nowhere else.
  - `expiresAt` string, date-time: When the key's maximum lifetime ends.
  - `id` string: The session key's id, as GET /api/api-keys lists it, so a client can recognize its own key.
  - `idleExpiresAt` string, date-time: When the key expires unless it is used first. Each request moves it forward.
  - `mfaEnrollmentRequired` boolean: Whether the user must enroll in MFA first. If so, the key can only enroll with POST /api/users/me/mfa and POST /api/users/me/mfa/confirm, and sign out.
  - `passwordChangeRequired` boolean: Whether the password must be changed first. If so, the key can only change the password with PUT /api/users/me/password and sign out.
  - `scopes` string[]
  - `username` string

## Other responses

- `400`: Bad Request
- `401`: The challenge is unknown, used, or expired, or the code is not valid.
- `403`: The user's MFA is locked after repeated bad codes.
- `404`: Password sign-in is not enabled.
- `406`: No representation matches Accept.
- `413`: Content Too Large
- `415`: Unsupported Media Type
- `429`: The username is locked after repeated failures (POST /api/sign-in only), or the client address is over SIGN_IN_RATE_LIMIT_PER_MINUTE. Retry-After gives the seconds to wait, and reason says which: locked or rate_limited.
- `500`: Internal Server Error
- `503`: Service Unavailable

## Changes

- **2026-10-23** `92f2a1031445`: 1 info
  - the response property `apiKey` became required for the status `200`
- **2026-10-10** `c4f85daa9208`: 14 info
  - added the optional property `field` to the response with the `400` status
  - added the optional property `field` to the response with the `401` status
  - added the optional property `field` to the response with the `403` status
  - added the optional property `field` to the response with the `413` status
  - …10 more
- **2026-10-06** `0390610d039d`: 1 info
  - added the optional property `id` to the response with the `200` status
- **2026-10-05** `4082c17721d8`: 5 breaking, 5 info
  - removed the media type `*/*` for the response with the status `400`
  - removed the media type `*/*` for the response with the status `413`
  - removed the media type `*/*` for the response with the status `415`
  - removed the media type `*/*` for the response with the status `500`
  - …6 more
- **2026-10-05** `ca5afc10b4a4`: 1 info
  - added the optional property `reason` to the response with the `429` status

[Operation history](https://skmtc.dev/philterd/apis/philter-api/changes/api/sign-in/mfa/post.md)

---

[API](https://skmtc.dev/philterd/apis/philter-api.md) - [All operations](https://skmtc.dev/philterd/apis/philter-api/llms.txt) - [OpenAPI document](https://skmtc.dev/philterd/apis/philter-api/revisions/aa3d7ad1aab4?raw)
