# Audit

The organization's append-only log of what happened and who did it, and the request timeline it tells.

Events are never changed or deleted. [List audit events](#listAuditEvents)
pages by cursor: pass `next_cursor` back as `cursor` with the same filters
until a page has no `next_cursor`.

## List audit events

`GET /api/v1/audit`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `audit.read`

Hosted edition only. The open edition answers 404.

Returns one page of the organization's audit log, newest first by default. Every filter is optional and they combine.

Paging is by cursor. When more events follow, the response carries `next_cursor`. Pass it back as `cursor` with the same sort, order and filters to get the next page. The last page has no `next_cursor`. `total_count` is how many events match the filters across every page.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `sort` | query | string | no | What to sort by. Events with the same value are ordered newest first. |
| `order` | query | string | no | `desc` for newest first, `asc` for oldest first. |
| `cursor` | query | string | no | The `next_cursor` from the previous page. Use the same sort, order and filters. |
| `q` | query | string | no | Free text. Matches the event type, the actor's name and id, the subject id, the event id, and the event's data. Case does not matter. |
| `event_type` | query | array | no | Only events of these types. Repeat the parameter to pass several. |
| `actor_type` | query | string | no | Only events by this kind of actor. |
| `actor_id` | query | string | no | Only events by one actor. A membership id also matches events recorded under its user id, and the other way round. An agent is given by its slug. |
| `subject_type` | query | string | no | Only events about this kind of thing, such as a request or a membership. |
| `subject_id` | query | string | no | Only events about one subject. Requires `subject_type`. An agent subject is given by its slug; every other subject by its id. |
| `outcome` | query | string | no | Only events whose action ended this way. Resolved from the event's data, so you do not need to know its shape. |
| `from_at` | query | date-time | no | Only events at or after this time. |
| `to_at` | query | date-time | no | Only events before this time. |
| `limit` | query | integer | no | How many events to return. Defaults to 100. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | object | One page of events |
| 400 | ErrorResponse | A filter is malformed, or `subject_id` was given without `subject_type` |
| 401 | ErrorResponse | You are not signed in |
| 403 | ErrorResponse | You cannot read the audit log |

Response body (200):

- `events` · array<AuditEvent> · required: The events on this page.
  - `sequence` · int64 · required: The event's position in the organization's log. Later events have higher numbers.
  - `id` · uuid · required
  - `organization_id` · uuid · required
  - `event_type` · string · required: What happened, as a dotted key such as `membership.suspended`.
  - `actor_type` · string · required: Who did it: `human`, `agent`, `system`, or `api_key`.
  - `actor_id` · uuid: The person, agent, or key that acted. Absent for `system`.
  - `actor_display_name` · string: The actor's name at the time.
  - `subject_type` · string · required: What kind of thing the event is about, such as a request or a membership.
  - `subject_id` · uuid · required: The thing the event is about.
  - `data` · any · required: Details specific to the event type, as JSON.
  - `occurred_at` · date-time · required: When it happened.
- `next_cursor` · string: Present when another page follows.
- `total_count` · int64 · required: How many events match the filters, across every page.

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/audit" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "events": [
    {
      "sequence": 1,
      "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "event_type": "string",
      "actor_type": "string",
      "actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "actor_display_name": "string",
      "subject_type": "string",
      "subject_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "data": {},
      "occurred_at": "2026-09-08T12:02:11Z"
    }
  ],
  "next_cursor": "string",
  "total_count": 1
}
```

## Retrieve an audit event

`GET /api/v1/audit/{id}`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `audit.read`

Hosted edition only. The open edition answers 404.

One entry of the organization's audit log by id, the record behind a deep link. An id from another organization is reported as not found.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes | The audit event's id. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | AuditEvent | The event |
| 400 | ErrorResponse | The id is not a UUID |
| 401 | ErrorResponse | You are not signed in |
| 403 | ErrorResponse | You cannot read the audit log |
| 404 | ErrorResponse | No such event in this organization |

Response body (200):

- `sequence` · int64 · required: The event's position in the organization's log. Later events have higher numbers.
- `id` · uuid · required
- `organization_id` · uuid · required
- `event_type` · string · required: What happened, as a dotted key such as `membership.suspended`.
- `actor_type` · string · required: Who did it: `human`, `agent`, `system`, or `api_key`.
- `actor_id` · uuid: The person, agent, or key that acted. Absent for `system`.
- `actor_display_name` · string: The actor's name at the time.
- `subject_type` · string · required: What kind of thing the event is about, such as a request or a membership.
- `subject_id` · uuid · required: The thing the event is about.
- `data` · any · required: Details specific to the event type, as JSON.
- `occurred_at` · date-time · required: When it happened.

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/audit/{id}" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "sequence": 1,
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "event_type": "string",
  "actor_type": "string",
  "actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "actor_display_name": "string",
  "subject_type": "string",
  "subject_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "data": {},
  "occurred_at": "2026-09-08T12:02:11Z"
}
```

## List the event types this organization has recorded

`GET /api/v1/audit/event_types`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `audit.read`

Hosted edition only. The open edition answers 404.

The vocabulary for the audit log's event filter, sorted. Unlike a fixed list it can never name a type that does not occur.

### Request

No parameters or body.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | object | Distinct event types |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |

Response body (200):

- `event_types` · array<string> · required

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/audit/event_types" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "event_types": [
    "string"
  ]
}
```
