
# Get a user's sign-in state

`GET /v1/users/{user_id}/sign-in-state` · permission `users:read`

Returns what your support team needs when a user cannot sign in: the user's open account lockouts, the failure counts that are still running on each sign-in path, and the last successful authentication. The call only reads; nothing is changed and no audit record is written. To remove the lockouts, use [`POST /v1/users/{user_id}:unlock`](unlock.md).

The response never carries the counter's internal subject, an email address, an IP address, a device or a session id. For the user's sessions, use [`GET /v1/users/{user_id}/sessions`](list-sessions.md).

## Organization scope

Pass `org_id` to build a member diagnostics view for an organization admin. The key then also needs the `organizations:read` permission; without it the request fails with `403 insufficient_permissions` before anything is read. If the user is not a member of that organization (no `invited`, `active` or `suspended` membership), the request fails with `404 not_found`, the same response as for an unknown user. If the user is a member, `last_sign_in` comes only from sessions in the context of that organization, and `failures` is always an empty array, because failure counters are not attributed to any organization. Locks are still returned, since a lock stops the account's sign-in in every context. Without `org_id` the response is unchanged.

## Request

| Parameter | In | Required | Description |
|---|---|---|---|
| `user_id` | path | yes | User id, prefixed `usr_`. |
| `org_id` | query | no | Organization id, prefixed `org_`. Limits the view to that organization; see [Organization scope](#organization-scope). Needs `organizations:read`. |

## Response

`200 OK`.

```json
{
  "locked": true,
  "locked_until": "2026-10-20T10:00:00Z",
  "locks": [
    {"id": "lck_0a1b2c3d4e5f6g7h8j9k0m1n2p", "path": "password",
     "locked_at": "2026-10-20T09:45:00Z", "expires_at": "2026-10-20T10:00:00Z"}
  ],
  "failures": [
    {"path": "totp", "count": 3, "window_ends_at": "2026-10-20T09:55:00Z"}
  ],
  "last_sign_in": {"authenticated_at": "2026-10-19T18:12:04Z", "amr": ["pwd", "otp"], "country": "DE"}
}
```

| Field | Description |
|---|---|
| `locked` | `true` when at least one sign-in path is locked. |
| `locked_until` | When the last open lockout ends; `null` without a lockout. |
| `locks` | Open lockouts, oldest first, at most one per path (`password`, `totp`, `sms_2fa`, `email_2fa`, `otp`, `recovery_code`). |
| `failures` | Failed attempts since the last success or the last lockout, per path, while the counting window runs. After `window_ends_at` the count starts over. The password window is the environment's `brute_force.window_minutes`. |
| `last_sign_in` | The most recent successful authentication among the sessions still kept: `authenticated_at` (a re-authentication in the same session moves it forward), the authentication methods (`amr`, RFC 8176) and the session's last known country (`null` when unknown). `null` when no session is kept: the user never signed in, or the last session ended more than 30 days ago. |

## Errors

Every error is an `application/problem+json` body with a stable `code`.

| HTTP | `code` | When |
|---|---|---|
| 404 | `not_found` | The user does not exist in this environment (or the id is malformed), or `org_id` is given and the user is not a member of that organization. |
| 401 | `unauthenticated` | The API key is missing, invalid or revoked. |
| 403 | `insufficient_permissions` | The key lacks the `users:read` permission, or `org_id` is given and the key lacks `organizations:read`. |
| 429 | `rate_limited` | The rate limit was exceeded; honor `Retry-After`. |
| 503 | `service_unavailable` | A dependency is temporarily unavailable; retry later. |

## Example

```bash
curl https://api.eu.keenauth.com/v1/users/usr_0a1b2c3d4e5f6g7h8j9k0m1n2p/sign-in-state \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY"
```

CLI: `keenauth users sign-in-state usr_0a1b2c3d4e5f6g7h8j9k0m1n2p`
