
# Inactive members report

`GET /v1/reports/inactive-members` · permission `reports:read`

Lists the active members of an organization who have had no successful sign-in in that organization for the last `days` days. Use it to find accounts to remove when a connection has no SCIM provisioning: review the list, then suspend the selected memberships with `POST /v1/organizations/{org_id}/memberships/{membership_id}:suspend`.

The environment always comes from the API key; there is no environment parameter. Rows contain only identifiers and timestamps, no name, email address, IP address or `User-Agent`. Because user identifiers are personal data, the report is limited to keys with `reports:read`.

## What counts as a sign-in

A successful authentication counts when it completed in the context of this organization. A sign-in without an organization selected does not count. A membership whose activation (`activated_at`, or its creation time when it was never activated) is newer than the threshold is not listed, so a member who joined yesterday never shows up in a 30-day report.

## Request

| Parameter | In | Required | Description |
|---|---|---|---|
| `organization_id` | query | yes | The organization (`org_…`). |
| `days` | query | no | Inactivity threshold in days, 7 to 365. Default 30. |
| `limit` | query | no | Page size, 1 to 100. Default 50. |
| `cursor` | query | no | `next_cursor` of the previous page. Valid only for the environment, `organization_id` and `days` it was issued for. |

Rows follow the order in which memberships were last updated. The threshold (`inactive_since`) is calculated once, on the first page, and carried in the cursor, so every page of one report uses the same instant.

A page scans at most 500 memberships and can return fewer rows than `limit`, even none. Keep following `next_cursor` until it is `null`.

## Response

`200 OK`.

| Field | Description |
|---|---|
| `organization_id` | The organization of the report. |
| `days` | The threshold used. |
| `inactive_since` | The threshold instant (request time minus `days`, RFC 3339, UTC). Members with no sign-in since then are listed. |
| `data_since` | Time of the first successful authentication recorded for the environment; sign-ins before it are unknown. `null` when the environment has no record yet. |
| `complete` | `true` when `data_since` is not after `inactive_since`, so the list covers the whole threshold window. |
| `data` | The rows on this page (see below). |
| `next_cursor` | Cursor for the next page, `null` on the last page. |

Each row in `data`:

| Field | Description |
|---|---|
| `membership_id` | `mem_…`. |
| `user_id` | `usr_…`. |
| `activated_at` | When the membership became active (its creation time when it was never activated). |
| `last_authenticated_at` | Last successful authentication in this organization, or `null` when none has been recorded since `data_since`. |

When `complete` is `false`, a listed member may have signed in before `data_since`, so do not treat the list as final. When `data_since` is `null`, no sign-in has been recorded for the environment yet and every member old enough is listed.

```json
{
  "organization_id": "org_01j9xk3m7q8r2s4t6v8w0y2z4d",
  "days": 30,
  "inactive_since": "2027-02-03T09:30:00Z",
  "data_since": "2026-12-01T08:00:00Z",
  "complete": true,
  "data": [
    {
      "membership_id": "mem_01j9xk3m7q8r2s4t6v8w0y2z4e",
      "user_id": "usr_01j9xk3m7q8r2s4t6v8w0y2z4f",
      "activated_at": "2026-12-10T10:00:00Z",
      "last_authenticated_at": "2027-01-05T17:45:00Z"
    }
  ],
  "next_cursor": null
}
```

## Errors

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

| HTTP | `code` | When |
|---|---|---|
| 400 | `invalid_cursor` | The cursor is malformed or was issued for another environment, organization or `days`. |
| 404 | `not_found` | The organization does not exist, or belongs to another environment. |
| 422 | `validation_failed` | `organization_id` is missing or malformed, `days` or `limit` is out of range, or a parameter is repeated. |
| 401 | `unauthenticated` | The API key is missing, invalid or revoked. |
| 403 | `insufficient_permissions` | The key lacks `reports:read`. |
| 429 | `rate_limited` | The rate limit was exceeded; honor `Retry-After`. |
| 503 | `service_unavailable` | The reporting service is temporarily unavailable; retry later. |

## Example

```bash
curl "https://api.eu.keenauth.com/v1/reports/inactive-members?organization_id=org_01j9xk3m7q8r2s4t6v8w0y2z4d&days=60" \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY"
```

```bash
keenauth reports inactive-members org_01j9xk3m7q8r2s4t6v8w0y2z4d --days 60
```

## Personal data

This endpoint carries personal data: organization memberships and invitations (user and membership identifiers with sign-in times). [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.
