---
title: Audit Log Reference
path: reference/audit-log
status: published
---

# Audit Log Reference

Audit log tracks every state-changing action across the platform. Tenant admins see their tenant's logs; platform admins see all logs across tenants.

**Base path:** `/api/v1/admin/audit-logs/`

## GET /api/v1/admin/audit-logs/

List audit log entries with pagination and filtering.

**Query parameters:**

| Param | Type | Notes |
|-------|------|-------|
| `page`, `page_size` | integer | Standard pagination |
| `tenant_id` | string | Platform admins only; omit for tenant admins |
| `actor_type` | string | `user`, `api_key`, `system`, `webhook` |
| `actor_id` | string | User UUID or API key UUID |
| `action` | string | e.g., `domain.create`, `record.update`, `dnssec.enable` |
| `resource_type` | string | `domain`, `record`, `api_key`, `role`, ... |
| `resource_id` | string | Specific resource UUID |
| `start_date` | ISO 8601 | Filter from this timestamp |
| `end_date` | ISO 8601 | Filter to this timestamp |

**Response:**

```json
{
  "data": [
    {
      "id": "log_abc123",
      "tenant_id": "t_xyz",
      "actor_type": "user",
      "actor_id": "u_abc",
      "actor_email": "user@example.com",
      "action": "record.create",
      "resource_type": "record",
      "resource_id": "r_xyz789",
      "resource_name": "www.example.com A",
      "details": {
        "before": null,
        "after": {"name": "www", "type": "A", "content": "192.0.2.10", "ttl": 300}
      },
      "ip_address": "203.0.113.42",
      "user_agent": "curl/8.0.1",
      "timestamp": "2026-04-23T10:42:00Z"
    }
  ],
  "total": 128,
  "page": 1,
  "page_size": 50
}
```

## GET /api/v1/admin/audit-logs/{log_id}

Get a single log entry.

**Response:** Same shape as items in the list response, with fuller `details`.

## Actor types

| Actor type | Actor ID | When |
|------------|----------|------|
| `user` | User UUID | Human-initiated via JWT |
| `api_key` | API key UUID | Machine-initiated via X-API-Key |
| `system` | `null` or a system service name | Background workers, scheduled tasks |
| `webhook` | ScaiKey event ID | Identity sync from ScaiKey |

For `api_key` actors, the log also carries the owning user or group ID. Audit questions like "which key did this?" are answerable directly.

## Action taxonomy

Actions follow `<resource_type>.<verb>`:

| Resource type | Typical actions |
|---------------|-----------------|
| `domain` | `create`, `update`, `delete`, `restore`, `export`, `validation.start`, `validation.check`, `validation.success`, `validation.failure`, `validation.cancel` |
| `record` | `create`, `update`, `delete`, `bulk_create`, `bulk_delete` |
| `dnssec` | `enable`, `disable`, `rotate_ksk`, `rotate_zsk`, `confirm_ds_published` |
| `api_key` | `create`, `revoke`, `activate`, `regenerate`, `delete`, `use` |
| `role` | `create`, `update`, `delete`, `assign`, `revoke` |
| `access_grant` | `create`, `update`, `delete` |
| `tenant` | `create`, `update`, `delete` (platform admin) |
| `user` | `create`, `update`, `delete`, `login`, `password_reset` |
| `template` | `create`, `update`, `delete`, `apply` |

The list is not exhaustive — new actions are added as endpoints evolve. Filter loosely (`action=record.*` via prefix matching is *not* supported; list them explicitly if filtering multiple actions).

## Scoping

- **Platform admins** see all logs by default. Filter with `tenant_id` to scope.
- **Tenant admins** see logs for their tenant only. `tenant_id` is forced to their tenant regardless of the query parameter.
- **Non-admins** get `403`.

## Retention

Audit logs are retained per platform policy — typically 90–730 days, depending on instance configuration. Export logs you need to keep longer before they roll off.

## Export

There's no dedicated export endpoint — paginate through `GET /audit-logs/` and serialize client-side.

```python
import os, httpx

HEADERS = {"X-API-Key": os.environ["SCAIDNS_API_KEY"]}
out = []
page = 1
while True:
    resp = httpx.get(
        "https://scaidns.scailabs.ai/api/v1/admin/audit-logs/",
        headers=HEADERS,
        params={"page": page, "page_size": 100, "start_date": "2026-01-01T00:00:00Z"},
    )
    data = resp.json()
    out.extend(data["data"])
    if page * 100 >= data["total"]:
        break
    page += 1

# Write to file, ship to SIEM, etc.
```

## Error codes

| Status | Meaning |
|--------|---------|
| `403` | Caller isn't an admin |
| `404` | Log entry not found (or in a tenant you can't see) |

## Related

- [Permissions and Access](../concepts/permissions-and-access.md) — who can see what.
- [Administration](./admin.md) — other admin-only endpoints.
