---
title: "Sign in."
method: POST
path: "/api/sign-in"
tags: ["Sign-in"]
---

# Sign in.

`POST /api/sign-in`

## Request

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

Takes a username and password. For a user enrolled in MFA, returns a single-use challenge that expires in five minutes; send it with a code to POST /api/sign-in/mfa to get the key. Otherwise returns a session key for the user, which expires after SESSION_KEY_IDLE_TIMEOUT_MINUTES without a request or SESSION_KEY_MAX_LIFETIME_MINUTES after issue. The key holds every scope, with the user's role still deciding administrator access, but cannot create API keys. A user who must change their password, or must enroll in MFA, gets a key that can only do that and sign out. A wrong password, an unknown user, a user without a password, and a deactivated user all get the same 401. Requires no API key. Returns 404 unless PASSWORD_SIGN_IN_ENABLED is true. Recorded as sign_in_succeeded or sign_in_failed with the username and client IP address, never the password.

## Request body

- SignInRequest
  - `password` string: The user's password. Never logged or audited.
  - `username` string

## Response `200`

Signed in, or, for a user enrolled in MFA, a challenge. A session key is returned here and nowhere else.

- union
  - 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
  - SignInChallengeResponse
    - `challenge` string, required: Single-use. Any attempt with it, right or wrong, uses it up.
    - `challengeExpiresAt` string, date-time
    - `mfaRequired` boolean: Always true: send the challenge and a code to POST /api/sign-in/mfa.

## Other responses

- `400`: Bad Request
- `401`: The username or password is not valid.
- `403`: The password is right, but the user's MFA is locked after repeated bad codes. An administrator must unlock it.
- `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`: 2 info
  - the response property `oneOf[#/components/schemas/SignInChallengeResponse]/challenge` became required for the status `200`
  - the response property `oneOf[#/components/schemas/SignInResponse]/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 `oneOf[#/components/schemas/SignInResponse]/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/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)
