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.
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.
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. Needs organizations:read. |
Response
200 OK.
{
"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
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