> ## Documentation Index
> Fetch the complete documentation index at: https://docs.perplexity.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Audit API

> Pull your organization's audit events into your SIEM or compliance tooling with a read-only, organization-scoped API key.

The Audit API lets enterprise organization admins read the same audit events that Perplexity delivers through [audit log webhooks](https://www.perplexity.ai/help-center/en/articles/11652747-audit-logs), on a pull basis. Point a SIEM collector or a scheduled job at a single endpoint, page through events newest-first, and poll on your own cadence.

> [!NOTE]
>
> The Audit API uses a dedicated **audit log API key**, separate from the Sonar API keys used for chat completions, search, and other developer endpoints. Sonar API keys cannot call this endpoint.

## Prerequisites

* An Enterprise organization with **audit logging enabled**. Org admins turn this on under **Organization Settings → Data and privacy → Audit Log Settings**. See the [Audit logs help center article](https://www.perplexity.ai/help-center/en/articles/11652747-audit-logs) for eligibility and setup.
* An organization admin to create and manage keys.

Audit logging must be on before you can create a key, and if it is later turned off the API returns `404` until it is re-enabled. Events are recorded only while audit logging is on, so there is no history from before you enabled it.

## Create an audit log API key

1. In the Perplexity web app, open your organization's settings and go to **Enterprise APIs**.
2. Under **Audit API → Audit log API keys**, select **Create key**, give the key a name you will recognize later (for example, `SIEM production`), and confirm.
3. Copy the key immediately. The full key is shown only once; afterward the list shows only the key's name and creation date.
4. Store the key in a secrets manager and treat it like any other credential.

Key properties:

* **Scope.** Keys are read-only and limited to the `audit_logs:read` scope. They cannot change settings, manage members, or call other Perplexity APIs.
* **Organization-bound.** A key reads events for the organization it was created in and nothing else.
* **Revocation.** Org admins can revoke a key from the same page. Revoked keys stop working immediately, and requests with a revoked key return `401`.
* **Audited.** Every successful read through the API produces an `audit_events_accessed` event in your audit log (see [Access events](#access-events)).

> [!NOTE]
>
> If the **Create key** button is unavailable or you see "API key creation is unavailable for this organization," contact your Perplexity account team to enable the Audit API for your organization.

## Make a request

Send the key as a bearer token to `GET https://api.perplexity.ai/v1/admin/audit-events`.

```bash cURL theme={null}
curl --request GET \
  --url "https://api.perplexity.ai/v1/admin/audit-events?limit=100" \
  --header "Authorization: Bearer $PERPLEXITY_AUDIT_API_KEY"
```

```python Python theme={null}
import os
import requests

response = requests.get(
    "https://api.perplexity.ai/v1/admin/audit-events",
    headers={"Authorization": f"Bearer {os.environ['PERPLEXITY_AUDIT_API_KEY']}"},
    params={"limit": 100},
    timeout=30,
)
response.raise_for_status()
page = response.json()
for event in page["data"]:
    print(event["created_at"], event["type"], event["actor_email"])
```

```typescript TypeScript theme={null}
const response = await fetch(
  "https://api.perplexity.ai/v1/admin/audit-events?limit=100",
  { headers: { Authorization: `Bearer ${process.env.PERPLEXITY_AUDIT_API_KEY}` } },
);
if (!response.ok) throw new Error(`Audit API error: ${response.status}`);
const page = await response.json();
for (const event of page.data) {
  console.log(event.created_at, event.type, event.actor_email);
}
```

Events are returned newest first. Without any time parameters, the endpoint covers the most recent 30 days.

### Query parameters

| Parameter | Type | Default | Description |
| - | - | - | - |
| `limit` | integer (1–500) | `100` | Maximum number of events to return in one page. |
| `start_time` | RFC 3339 timestamp | `end_time` − 30d | Inclusive lower bound on `created_at`. Must include a timezone offset. |
| `end_time` | RFC 3339 timestamp | now | Inclusive upper bound on `created_at`. Must include a timezone offset. The window between `start_time` and `end_time` cannot exceed 30 days. |
| `event_type` | string | — | Return only events of this type, for example `member_role_changed`. Required to see `audit_events_accessed` events, which the default feed omits. |
| `after_id` | string (cursor) | — | Return events **older** than this cursor. Use the previous page's `last_id` to page backward through history. |
| `before_id` | string (cursor) | — | Return events **newer** than this cursor. Use the `first_id` you saved from an earlier poll to fetch only what arrived since. Mutually exclusive with `after_id`. |

Cursors (`first_id`, `last_id`, `after_id`, `before_id`) are opaque strings. Store them and pass them back unchanged; do not parse them or build your own.

### Response

```json theme={null}
{
  "data": [
    {
      "id": "6a0d9a2c-3c6f-4e0e-9e2b-1a2b3c4d5e6f",
      "created_at": "2026-10-27T18:42:11.512000Z",
      "type": "member_role_changed",
      "actor_email": "admin@example.com",
      "actor_external_user_id": "",
      "ip_address": "203.0.113.42",
      "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6) AppleWebKit/605.1.15 Safari/605.1.15",
      "session_id": "7c1e0e2a-9f3d-4a2e-8c1e-4b9d6e2f1a3c",
      "metadata": {
        "target_user_email": "jordan@example.com",
        "target_external_user_id": "",
        "org_user_uuid": "0d2f0b1e-7a4c-4b6e-9f3a-2c1d5e6f7a8b",
        "old_role": "member",
        "new_role": "admin"
      }
    }
  ],
  "has_more": true,
  "first_id": "eyJ2ZXJzaW9uIjoxLCJ0aW1lc3RhbXAiOiIyMDI2LTEwLTI3VDE4OjQyOjExLjUxMjAwMCswMDowMCIsInV1aWQiOiI2YTBkOWEyYy0zYzZmLTRlMGUtOWUyYi0xYTJiM2M0ZDVlNmYifQ",
  "last_id": "eyJ2ZXJzaW9uIjoxLCJ0aW1lc3RhbXAiOiIyMDI2LTEwLTI3VDA5OjAzOjU0LjAwMTAwMCswMDowMCIsInV1aWQiOiIzZjJhMWIwYy1kNGU1LTRmNmEtOGI3Yy05ZDBlMWYyYTNiNGMifQ"
}
```

| Field | Description |
| - | - |
| `data` | Events in this page, newest first. |
| `has_more` | Whether more events exist beyond this page in the direction you are paging. Stop when it is `false`; do not wait for an empty page. |
| `first_id` | Cursor for the newest event in the page, or `null` when `data` is empty. Save it to resume forward polling later. |
| `last_id` | Cursor for the oldest event in the page, or `null` when `data` is empty. Pass it as `after_id` to fetch the next older page. |

### Event fields

| Field | Description |
| - | - |
| `id` | Stable, unique event identifier. |
| `created_at` | When the event occurred, in UTC. |
| `type` | Event type, for example `query`, `member_invited`, `connector_connected`. The set of types matches audit log webhooks; see the [Audit logs help center article](https://www.perplexity.ai/help-center/en/articles/11652747-audit-logs) for the catalog. |
| `actor_email` | Email of the member who performed the action. Empty for system-generated events, including `audit_events_accessed`. |
| `actor_external_user_id` | The actor's external identifier from your identity provider (SCIM `externalId`) when one is set; otherwise an empty string. |
| `ip_address` | Client IP address recorded for the action, or an empty string when unavailable. |
| `user_agent` | Client user agent recorded for the action, or an empty string when unavailable. |
| `session_id` | Identifier for the actor's sign-in session, so you can group actions from one session. `null` when no session applies. |
| `metadata` | Event-specific details. Contents vary by `type`, and keys are the same ones delivered in the audit log webhook payload for that event. Treat the object as additive: new keys may appear over time without a version change. |

Metadata carries the same content as webhooks, including, where applicable, the truncated query text for `query` events, response text for `answer_generated` events, and file names for `file_upload` events. Secret values are never included; for example, when an admin updates a webhook URL or secret, the `organization_settings_update` event records that the field changed and redacts the value.

## Paginate and poll

Audit events are append-only and ordered by `created_at`. The patterns below work well for a scheduled collector.

### Backfill a time window

Fix `start_time` and `end_time` for the whole run, then follow `last_id` until `has_more` is `false`.

```python theme={null}
import os
import requests

BASE = "https://api.perplexity.ai/v1/admin/audit-events"
HEADERS = {"Authorization": f"Bearer {os.environ['PERPLEXITY_AUDIT_API_KEY']}"}

def backfill(start_time: str, end_time: str):
    params = {"start_time": start_time, "end_time": end_time, "limit": 500}
    while True:
        page = requests.get(BASE, headers=HEADERS, params=params, timeout=30)
        page.raise_for_status()
        body = page.json()
        yield from body["data"]
        if not body["has_more"]:
            return
        params["after_id"] = body["last_id"]

for event in backfill("2026-10-01T00:00:00Z", "2026-10-31T00:00:00Z"):
    ...  # write to your SIEM
```

Each request can cover at most 30 days. Split longer ranges into consecutive 30-day windows.

### Poll for new events

For ongoing collection, keep a checkpoint and ask for events newer than it:

1. On each cycle, capture the current time as `end_time` and keep it fixed for every page in that cycle.
2. Pass the `first_id` you saved from the previous cycle as `before_id`.
3. Page with `after_id` and `last_id` until `has_more` is `false`.
4. Store the newest `first_id` you saw as the next checkpoint. If `data` was empty, keep the old checkpoint.

```python theme={null}
def poll(checkpoint: str | None, end_time: str):
    params = {"end_time": end_time, "limit": 500}
    if checkpoint:
        params["before_id"] = checkpoint
    newest = checkpoint
    while True:
        page = requests.get(BASE, headers=HEADERS, params=params, timeout=30)
        page.raise_for_status()
        body = page.json()
        if body["first_id"] and newest == checkpoint:
            newest = body["first_id"]
        yield from body["data"]
        if not body["has_more"]:
            return newest
        params["after_id"] = body["last_id"]
```

Events can arrive a short time after they occur. Poll every few minutes rather than continuously, and deduplicate on `id` if you overlap windows for safety.

## Access events

Every successful read through the Audit API is itself recorded as an `audit_events_accessed` event. These events are system-generated, so `actor_email`, `ip_address`, and `user_agent` are empty and `session_id` is `null`. The `metadata` identifies the key and the request:

```json theme={null}
{
  "id": "b7a1f0e4-2d3c-4c5b-9a8f-0e1d2c3b4a59",
  "created_at": "2026-10-27T19:00:02.104000Z",
  "type": "audit_events_accessed",
  "actor_email": "",
  "actor_external_user_id": "",
  "ip_address": "",
  "user_agent": "",
  "session_id": null,
  "metadata": {
    "api_key_id": "5f6e7d8c-9b0a-4c1d-8e2f-3a4b5c6d7e8f",
    "operation": "audit_events.list",
    "result_count": 100
  }
}
```

So that a collector does not generate a feed that is mostly its own reads, the default response omits `audit_events_accessed` events. Request them explicitly with `event_type=audit_events_accessed` when you want to review who has been reading your audit log.

## Rate limits and errors

Each organization may make up to **2 requests per second** to this endpoint across all of its audit log API keys. Requests beyond that return `429` with a `Retry-After` header; wait at least that long before retrying. Use `limit=500` to minimize the number of calls needed.

Errors use the standard API error envelope:

```json theme={null}
{
  "error": {
    "message": "The requested time range cannot exceed 30 days.",
    "type": "invalid_request",
    "code": 400
  }
}
```

| Status | `type` | Cause |
| - | - | - |
| `400` | `invalid_request` | Missing timezone on `start_time`/`end_time`, a window longer than 30 days, an invalid cursor, or `after_id` and `before_id` together. |
| `400` | `bad_request` | A parameter failed validation, for example `limit` outside 1–500 or an unparseable timestamp. |
| `401` | `unauthorized` | Missing, invalid, or revoked key. |
| `403` | `permission_denied` | The key does not have the `audit_logs:read` scope (for example, a Sonar API key was used). |
| `404` | `not_found` | Audit logging is not enabled for the organization. |
| `429` | `request_rate_limit_exceeded` | More than 2 requests per second for the organization. Honor the `Retry-After` header. |
| `503` | `service_unavailable` | The audit log store is temporarily unavailable. Retry with backoff. |

Every attempt, including rejected ones, is logged on Perplexity's side for security review.

## Webhooks or API?

Both deliver the same events with the same metadata. Use whichever fits your tooling, or both:

| | Audit log webhooks | Audit API |
| - | - | - |
| Delivery | Perplexity pushes each event to your HTTPS endpoint as it happens | You pull pages on your schedule |
| Best for | Near-real-time alerting, SIEMs with a webhook receiver | Collectors that poll, backfills, air-gapped ingestion through a scheduled job |
| Setup | Public HTTPS endpoint and optional authorization header | One API key, no inbound endpoint |
| Historical reads | Only events delivered since the webhook was configured | Re-read any 30-day window on demand |

> [!NOTE]
>
> The Audit API is distinct from the [Analytics API](https://docs.perplexity.ai/docs/admin/computer-analytics-api.md), which reports Computer usage and uses its own organization analytics API key.
