
# List a user's authenticators

`GET /v1/users/{user_id}/authenticators` · permission `users:read`

Lists the authenticators (passkeys, TOTP, SMS, email and push factors) of a user in one page, including those revoked in the last 90 days, ordered by creation time. The number of authenticators per user is bounded, so the list has no `limit`, `after`, `before` or `order`, and `list_metadata` is always `null` valued. Only metadata is returned: never public keys, credential ids, TOTP secrets or recovery codes.

## 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, only the class of each authenticator and whether MFA is enrolled are shown: only active authenticators are listed, and `label`, `last_used_at`, `revoked_at`, `revoked_by` and `revoke_reason` are `null` and `synced` is `false`, because the person's device details outside the organization do not belong to it. MFA is enrolled when an active authenticator has the `role` `second_factor` or `both`. 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`. `data` holds the authenticators ordered by creation time; `list_metadata` is always `null` valued because the list is not paginated. Empty fields are `null`.

When the key lacks the `users:read_pii` permission, `label` is reduced to initials (`A. L.`).

```json
{
  "data": [
    {
      "id": "cred_0a1b2c3d4e5f6g7h8j9k0m1n2p",
      "type": "webauthn",
      "role": "both",
      "label": "Laptop",
      "status": "active",
      "synced": true,
      "created_at": "2026-01-15T09:30:00Z",
      "last_used_at": "2026-02-01T08:00:00Z",
      "revoked_at": null,
      "revoked_by": null,
      "revoke_reason": null
    }
  ],
  "list_metadata": {
    "before": null,
    "after": null
  }
}
```

`type`: `webauthn`, `totp`, `sms`, `email`, `push` · `role`: `first_factor`, `second_factor`, `both` · `status`: `pending`, `active`, `revoked`, `clone_suspected`

## 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/authenticators \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY"
```

## Personal data

This endpoint carries personal data: credentials. [Personal data in the KeenAuth APIs](/docs/policies/personal-data) describes what is stored, how long it is kept and how it is deleted. Do not send special categories of personal data (health, biometric data, religion, political opinions, sexual orientation) in any field.
