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

# Audit logs

> A verifiable, immutable record of who did what in your account, and from where.

Audit logs give your team a tamper-proof record of the sensitive actions taken in
your account — keys created and revoked, domains and inboxes added or deleted,
webhooks changed. Each event captures *what* happened, *who* did it, and *where it
came from*, so you can answer a compliance or incident question after the fact
with a real answer.

<Note>
  Audit logs and API-key IP allowlisting are [enterprise governance
  features](/platform/enterprise-plans). Talk to us if they aren't enabled on your
  plan.
</Note>

## Read the trail

Fetch events from `GET /v1/audit-logs`, newest first. The call needs the
`account:read` [scope](/concepts/scopes). Events come back in the standard list
envelope under `data`, paginated with a cursor.

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl https://api.dairo.app/v1/audit-logs \
    -H "Authorization: Bearer $DAIRO_API_KEY"
  ```

  ```ts title="TypeScript" theme={null}
  const { data, pagination } = await dairo.auditLogs.list({ limit: 50 });
  for (const event of data) {
    console.log(event.createdAt, event.action, event.actor, event.ip);
  }
  ```

  ```python title="Python" theme={null}
  page = dairo.audit_logs.list(limit=50)
  for event in page.data:
      print(event.created_at, event.action, event.actor, event.ip)
  ```

  ```bash title="CLI" theme={null}
  dairo audit-logs list --limit 50 --json
  ```

  ```text title="MCP" theme={null}
  Tool: get_compliance_reports   (scope account:read)
  Args: { "action": "listAuditLogs", "limit": 50 }
  > "List the last 50 audit-log events and flag any keys created today."
  ```
</CodeGroup>

A page is the standard list envelope — `data` holds the events and `pagination`
carries the cursor for the next page:

```json theme={null}
{
  "object": "list",
  "data": [
    {
      "object": "audit_log",
      "id": "9f1c2b7e-7e3a-4c0b-9a8e-2f6d1c0b4a11",
      "action": "api_key.created",
      "resourceType": "api_key",
      "resourceId": "key_8f2a",
      "metadata": {},
      "actor": "key_a1b2",
      "ip": "203.0.113.4",
      "createdAt": "2026-06-10T14:22:03Z"
    }
  ],
  "pagination": { "nextCursor": "eyJvIjoxMjN9", "hasMore": true }
}
```

### Event fields

| Field          | Meaning                                                                                                         |
| -------------- | --------------------------------------------------------------------------------------------------------------- |
| `action`       | The audited action, e.g. `api_key.created`, `domain.deleted`, `webhook.created`.                                |
| `resourceType` | The kind of resource the action targeted, e.g. `api_key`, `domain`, `inbox`, `webhook`. May be null.            |
| `resourceId`   | Identifier of the affected resource, when one applies.                                                          |
| `metadata`     | Action-specific context recorded with the event. May be null.                                                   |
| `actor`        | Who performed the action — an API key (by id), `dashboard` for console actions, or a system actor. May be null. |
| `ip`           | Source IP the request came from, when captured.                                                                 |
| `createdAt`    | When the event was recorded, in UTC.                                                                            |

`metadata` holds action-specific context — some actions record a field or two and
others record none. An `inbox.created` event records the new inbox's `address`,
for example, while an `api_key.created` event records an empty object. Treat it as
free-form context, not a fixed schema.

### Actions you'll see

The trail records security-relevant actions. The most common, grouped by what they
target:

| `resourceType` | Actions                                                 |
| -------------- | ------------------------------------------------------- |
| `api_key`      | `api_key.created`, `api_key.revoked`                    |
| `domain`       | `domain.created`, `domain.deleted`                      |
| `inbox`        | `inbox.created`, `inbox.deleted`                        |
| `webhook`      | `webhook.created`, `webhook.deleted`                    |
| `audience`     | `audience.created`, `audience.deleted`, `audience.sent` |

<Note>
  The set of audited actions grows over time. Don't hard-code an exhaustive list:
  match on the prefix (`api_key.`, `domain.`) or read `resourceType`, and tolerate
  actions you don't recognize yet.
</Note>

### Page through history

Pass `limit` (1–100, default 25) and follow `pagination.nextCursor` the usual way —
see [pagination](/concepts/pagination). The trail is account-wide and newest-first;
filter by `action` or `resourceType` on each page in your own code.

<Tip>
  Audit events are append-only — there's no endpoint to edit or delete them, and
  that immutability is the point. If your retention policy needs a longer window
  than your plan keeps, schedule an export to your own archive for long-term
  retention.
</Tip>

## API-key IP allowlisting

For high-trust automation, you can lock an API key to a set of trusted IP
addresses, so a leaked secret is useless from anywhere else. It's a governance
control that sits on top of scopes: scopes limit *what* a key can do; the
allowlist limits *where* it can do it from.

### Set an allowlist

Send `allowedIps` when you create a key. Each entry is an IPv4 or IPv6 address, or
a CIDR range — up to 50 of them.

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl -X POST https://api.dairo.app/v1/api-keys \
    -H "Authorization: Bearer $DAIRO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
          "name": "ci-runner",
          "scopes": ["messages:send"],
          "allowedIps": ["203.0.113.0/24", "198.51.100.7"]
        }'
  ```

  ```ts title="TypeScript" theme={null}
  const { apiKey, secret } = await dairo.apiKeys.create({
    name: "ci-runner",
    scopes: ["messages:send"],
    allowedIps: ["203.0.113.0/24", "198.51.100.7"],
  });
  console.log(apiKey.allowedIps); // ["203.0.113.0/24", "198.51.100.7"]
  ```

  ```python title="Python" theme={null}
  result = dairo.api_keys.create(
      name="ci-runner",
      scopes=["messages:send"],
      allowed_ips=["203.0.113.0/24", "198.51.100.7"],
  )
  print(result.api_key.allowed_ips)
  ```

  ```bash title="CLI" theme={null}
  # --allowed-ip is repeatable (up to 50 entries)
  dairo api-key create --name ci-runner \
    --scope messages:send \
    --allowed-ip 203.0.113.0/24 \
    --allowed-ip 198.51.100.7
  ```

  ```text title="MCP" theme={null}
  Tool: manage_api_keys  (scope keys:write, confirm required)
  Args: {
    "action": "create",
    "name": "ci-runner",
    "scopes": ["messages:send"],
    "allowedIps": ["203.0.113.0/24", "198.51.100.7"],
    "confirm": true
  }
  ```
</CodeGroup>

The created key — and the key returned by the list endpoint — echoes its
`allowedIps`. A key with no allowlist returns an empty `allowedIps` array (`[]`)
and authenticates from any IP, which is the default.

### See where a key is locked to

The allowlist shows up wherever a key's details appear, so you can audit it
without re-creating the key. List your keys to see every key's `allowedIps` and
confirm each one is pinned to the IPs you expect.

<CodeGroup>
  ```bash title="cURL" theme={null}
  # Every key's allowlist
  curl https://api.dairo.app/v1/api-keys \
    -H "Authorization: Bearer $DAIRO_API_KEY"
  ```

  ```ts title="TypeScript" theme={null}
  const { data } = await dairo.apiKeys.list();
  for (const k of data) console.log(k.id, k.allowedIps); // [] = any IP
  ```

  ```bash title="CLI" theme={null}
  dairo api-key list --json   # allowedIps on each key
  ```
</CodeGroup>

### How enforcement works

On every authenticated request, Dairo resolves the caller's source IP and checks
it against the key's allowlist *before* the request does any work:

<Steps>
  <Step title="Resolve the source IP">
    The trusted client IP is the real connecting address, never a header the
    caller supplies — so it can't be spoofed.
  </Step>

  <Step title="Match against the allowlist">
    The IP must fall inside at least one configured address or CIDR range.
  </Step>

  <Step title="Allow or deny (deny-by-default)">
    A match proceeds. Anything else — an IP that can't be matched, or can't be
    resolved at all — is rejected with `403`, even when the bearer secret and scopes
    are otherwise valid. A correct secret from an untrusted network gets you nothing.
  </Step>
</Steps>

### Operating notes

<AccordionGroup>
  <Accordion title="Keys without an allowlist are unchanged">
    Omitting `allowedIps` (or sending `null`) keeps the default: the key
    authenticates from any IP. Allowlisting is strictly opt-in.
  </Accordion>

  <Accordion title="Pin egress, not office Wi-Fi">
    Allowlist the stable egress IPs of your servers, CI, or NAT gateway — not
    laptops on dynamic residential or café networks, which will lock themselves
    out when the IP rotates.
  </Accordion>

  <Accordion title="Allowlist changes are audited">
    Creating or revoking an allowlisted key is recorded in the
    [audit trail](#read-the-trail), so the control itself leaves a record.
  </Accordion>

  <Accordion title="Locked out? Rotate from a trusted IP">
    If your egress IP changes and a key can no longer reach the API, create a
    replacement key (with the new range) from a trusted network and revoke the old
    one. Dairo never weakens the check to recover access.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Tamper-evident audit export" icon="file-signature" href="/compliance/audit-export">
    A signed, verifiable export of the same trail for archival.
  </Card>

  <Card title="API keys & authentication" icon="key" href="/get-started/authentication">
    API keys, scopes, and per-surface auth.
  </Card>

  <Card title="API reference" icon="code" href="/api-reference">
    The full `GET /v1/audit-logs` and API-key schemas.
  </Card>

  <Card title="CLI" icon="terminal" href="/agent-first/cli">
    `dairo audit-logs list` and `dairo api-key create --allowed-ip`.
  </Card>
</CardGroup>
