# List Audit Log

> Who or what changed state in the workspace, newest first.

Requires an API key with the `read` scope.

The audit log merges three records FoxReach already keeps, read-only:

| `source` | What it records |
|----------|-----------------|
| `campaign_status` | Campaign state changes (`campaign.state_changed`) and sends blocked by a gate while a campaign stays active (`campaign.send_blocked`, one per reason, not one per attempt). |
| `account_connection` | Email account connection changes (`account.connection_changed`) with the cause, including each failed attempt that counts toward disconnecting. |
| `event` | Manual inbox thread changes (`audit.inbox_thread.categorized`, `audit.inbox_thread.deleted`), agency client changes (`audit.agency_client.created`, `.renamed`, `.deleted`), agency branding changes made with an API key (`audit.agency_branding.updated`), template sharing (`audit.template.published`, `.unpublished`), campaign changes made with an API key (`audit.campaign.duplicated`, `.shared`, `.unshared`, `.leads_removed`), team changes (`audit.workspace_member.invited`, `.role_changed`, `.removed`, `audit.workspace_invite.revoked`, `.resent`), API keys created or revoked (`audit.api_key.created`, `.revoked`), inbox notes added or deleted with an API key (`audit.inbox_note.created`, `.deleted`) and webhooks paused or resumed (`webhook.paused`, `webhook.resumed`). |

Only these event types are returned, and only the fields listed below. The `actor` is shown as stored: `system`, `system:<source>`, `user:<id>` or `api_key:<id>` (with `:agency:<workspace>` when the key was acting as a client workspace). Rows for an email account disappear when the account is deleted.

Paginate with the cursor: pass `meta.nextCursor` from one page as `cursor` on the next, until it is `null`. Rows that share a timestamp keep a stable order.

## Query Parameters

<ParamField query="pageSize" type="integer">Rows per page, 1 to 100. Default 50.</ParamField>
<ParamField query="cursor" type="string">The `meta.nextCursor` of the previous page.</ParamField>
<ParamField query="source" type="string">`campaign_status`, `account_connection` or `event`.</ParamField>
<ParamField query="since" type="string">Only rows at or after this ISO 8601 time.</ParamField>
<ParamField query="until" type="string">Only rows at or before this ISO 8601 time.</ParamField>
<ParamField query="actor" type="string">Only rows by exactly this actor string.</ParamField>
<ParamField query="campaignId" type="string">Only that campaign's `campaign_status` rows.</ParamField>
<ParamField query="accountId" type="string">Only that email account's `account_connection` rows.</ParamField>

## Response

<ResponseField name="data" type="array">
  <Expandable title="Entry">
    <ResponseField name="id" type="string">Row identifier.</ResponseField>
    <ResponseField name="at" type="string">ISO 8601 timestamp in UTC.</ResponseField>
    <ResponseField name="source" type="string">`campaign_status`, `account_connection` or `event`.</ResponseField>
    <ResponseField name="type" type="string">What happened, for example `campaign.state_changed`.</ResponseField>
    <ResponseField name="actor" type="string">Who or what did it, as stored. `null` on rows written before actors were recorded.</ResponseField>
    <ResponseField name="subject" type="object">`kind` (`campaign`, `email_account`, `reply`, `workspace`, `template`, `webhook`, `workspace_member`, `workspace_invite`, `api_key`, `inbox_note`) and the `id` it points to.</ResponseField>
    <ResponseField name="from" type="string">Previous state, category, name or role, or `null`.</ResponseField>
    <ResponseField name="to" type="string">New state, category, name or role, or `null`.</ResponseField>
    <ResponseField name="reason" type="string">The reason code, or the route that made the change.</ResponseField>
    <ResponseField name="detail" type="string">Extra text (a gate message, a provider error cut to 500 characters, or the action), or `null`.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="meta" type="object">`pageSize` and `nextCursor` (`null` on the last page).</ResponseField>

<RequestExample>
```bash cURL
curl "https://api.foxreach.io/api/v1/audit-log?source=account_connection&pageSize=20" \
  -H "X-API-Key: otr_your_key"
```

```python Python SDK
from foxreach import FoxReach

client = FoxReach(api_key="otr_your_key")
page = client.audit_log.list(source="account_connection", page_size=20)
for entry in page["data"]:
    print(entry["at"], entry["type"], entry["actor"])
```

```typescript TypeScript SDK
import { FoxReach } from "foxreach";

const client = new FoxReach({ apiKey: "otr_your_key" });
const page = await client.auditLog.list({ source: "account_connection", pageSize: 20 });
```
</RequestExample>

<ResponseExample>
```json
{
  "data": [
    {
      "id": "evt_abc123",
      "at": "2026-11-04T09:12:30+00:00",
      "source": "account_connection",
      "type": "account.connection_changed",
      "actor": "system:smtp_send",
      "subject": {"kind": "email_account", "id": "acc_abc123"},
      "from": "connected",
      "to": "error",
      "reason": "bad_credential",
      "detail": "Invalid credentials"
    }
  ],
  "meta": {"pageSize": 20, "nextCursor": null}
}
```
</ResponseExample>

Entries are not edited or deleted by the API. Agency actions that created, renamed or deleted a client are recorded on the agency workspace; template and webhook rows on the workspace they belong to.

## Errors

| Status | Description |
|--------|-------------|
| `401` | Missing, invalid, revoked or expired API key |
| `403` | API key does not have the required scope: `read` |
| `400` | `cursor is not valid` |
| `422` | `source` is not one of the three values, or `pageSize` is outside 1 to 100 |

## Rate Limit

- **300 requests per minute, shared by every `read` call in your workspace.** Fixed 60-second window.
- Every response includes `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (unix epoch seconds) and `X-RateLimit-Bucket` (`read`).
- A `429` response includes a `Retry-After` header (seconds until the bucket resets).

For client-side patterns, see [Rate Limiting](/rate-limiting).
