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

# Export Activity

> Pull the verdict log for review, reporting, and your own retention.

Every verdict is logged in Command. The Activity endpoints read that log for up to **30 days**. Use them for nightly exports, threshold tuning, filters by agent or source, or copying verdicts into your own warehouse for longer retention.

## Endpoints

```
GET /api/activities                 list, newest first, 30-day window max
GET /api/activities/{activity_id}   one record with timeline and rule detail
```

List filters (repeatable where noted) match [List Activity](https://docs.zerodrift.com/api-reference/activities/list-activities.md):

| Param | Meaning |
| - | - |
| `from` / `to` | Window as ISO 8601 date-times (max 30 days) |
| `status` | Repeatable: `auto_fixed`, `blocked`, `warned`, `passed` |
| `agent_id` | Repeatable agent id |
| `source` | Repeatable source |
| `rule_id` | Repeatable rule id |
| `search` | Free-text search |
| `cursor` | Opaque pagination cursor from a previous response |
| `limit` | Page size 1–100 |

List items include `activity_id`, `occurred_at`, `event_name`, `status`, `source`, `provider`, `model`, `agent` (`id`, `name`), `rules` (`id`, `name`, `severity`, `type`, `policy`), `rule_count`, and `truncated_rule_count`. The detail endpoint adds timeline and fuller rule fields.

`status` maps to product outcomes: `passed` ≈ Pass, `auto_fixed` ≈ Fix, `blocked` ≈ Block, `warned` ≈ delivered with caution.

## Nightly paginated export

Run once per night. Keep each request inside a 30-day window. Overlap the previous run (lookback) so records that appear after they occurred are not missed; dedupe by `activity_id`. Walk `next_cursor` until it is null. Encode `cursor` as a query value (`--data-urlencode` or an equivalent). Write CSV or NDJSON from the public list fields only.

```bash cURL theme={null}
# Last 24 hours plus a 6-hour lookback (keep from/to within 30 days)
FROM=$(date -u -d '30 hours ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
  || date -u -v-30H +%Y-%m-%dT%H:%M:%SZ)
TO=$(date -u +%Y-%m-%dT%H:%M:%SZ)
CURSOR=""
: > activity.ndjson

while true; do
  ARGS=(
    --data-urlencode "from=$FROM"
    --data-urlencode "to=$TO"
    --data-urlencode "limit=100"
    --data-urlencode "status=blocked"
    --data-urlencode "status=warned"
    --data-urlencode "status=auto_fixed"
    --data-urlencode "status=passed"
  )
  if [ -n "$CURSOR" ]; then
    ARGS+=(--data-urlencode "cursor=$CURSOR")
  fi
  PAGE=$(curl -sG "https://api.zerodrift.ai/api/activities" \
    -H "x-api-key: $ZERODRIFT_API_KEY" \
    "${ARGS[@]}")
  printf '%s' "$PAGE" | jq -c '.items[]' >> activity.ndjson
  CURSOR=$(printf '%s' "$PAGE" | jq -r '.next_cursor // empty')
  if [ -z "$CURSOR" ] || [ "$CURSOR" = "null" ]; then
    break
  fi
done

# Dedupe overlapping lookback windows by activity_id
jq -sc 'unique_by(.activity_id)[]' activity.ndjson > activity.unique.ndjson
wc -l activity.unique.ndjson
```

```python Python theme={null}
import csv
import os
from datetime import datetime, timedelta, timezone

import requests

API = "https://api.zerodrift.ai"
HEADERS = {"x-api-key": os.environ["ZERODRIFT_API_KEY"]}


def export_activity(days: int = 1, path: str = "activity.csv") -> int:
    if days > 30:
        raise ValueError("Activity windows are capped at 30 days")
    to = datetime.now(timezone.utc)
    # 6-hour lookback so late-visible records overlap yesterday's export
    # Clamp so from/to never exceeds the 30-day API cap
    frm = max(to - timedelta(days=days, hours=6), to - timedelta(days=30))
    params: dict = {
        "from": frm.isoformat(),
        "to": to.isoformat(),
        "limit": 100,
        # Repeatable filters — same names as List Activity
        "status": ["auto_fixed", "blocked", "warned", "passed"],
    }
    # Optional: params["agent_id"] = [os.environ["AGENT_ID"]]
    # Optional: params["source"] = ["api"]
    # Optional: params["rule_id"] = [os.environ["RULE_ID"]]
    # Optional: params["search"] = "guarantees"

    by_id: dict[str, dict] = {}
    cursor = None
    while True:
        if cursor:
            params["cursor"] = cursor
        page = requests.get(
            f"{API}/api/activities",
            headers=HEADERS,
            params=params,
            timeout=30,
        )
        page.raise_for_status()
        body = page.json()
        for a in body["items"]:
            by_id[a["activity_id"]] = {
                "activity_id": a["activity_id"],
                "occurred_at": a["occurred_at"],
                "event_name": a.get("event_name"),
                "status": a["status"],
                "source": a.get("source"),
                "provider": a.get("provider"),
                "model": a.get("model"),
                "agent_id": (a.get("agent") or {}).get("id"),
                "agent_name": (a.get("agent") or {}).get("name"),
                "rule_count": a.get("rule_count"),
                "rules": "; ".join(
                    r.get("name") or r.get("id") or ""
                    for r in a.get("rules") or []
                ),
            }
        cursor = body.get("next_cursor")
        if not cursor:
            break

    rows = list(by_id.values())
    fieldnames = [
        "activity_id",
        "occurred_at",
        "event_name",
        "status",
        "source",
        "provider",
        "model",
        "agent_id",
        "agent_name",
        "rule_count",
        "rules",
    ]
    with open(path, "w", newline="") as f:
        w = csv.DictWriter(f, fieldnames=fieldnames)
        w.writeheader()
        w.writerows(rows)
    return len(rows)


print(export_activity(days=1))
```

```typescript TypeScript theme={null}
const API = "https://api.zerodrift.ai";

type ActivityItem = {
  activity_id: string;
  occurred_at: string;
  event_name?: string;
  status: string;
  source?: string;
  provider?: string;
  model?: string;
  agent?: { id?: string; name?: string } | null;
  rule_count?: number;
  rules?: { id?: string; name?: string }[];
};

export async function exportActivity(days = 1) {
  if (days > 30) throw new Error("Activity windows are capped at 30 days");
  const to = new Date();
  // 6-hour lookback so late-visible records overlap yesterday's export
  // Clamp so from/to never exceeds the 30-day API cap
  const from = new Date(
    Math.max(
      to.getTime() - days * 86_400_000 - 6 * 3_600_000,
      to.getTime() - 30 * 86_400_000,
    ),
  );
  const byId = new Map<string, Record<string, string | number | undefined>>();
  let cursor: string | null = null;

  do {
    const url = new URL(`${API}/api/activities`);
    url.searchParams.set("from", from.toISOString());
    url.searchParams.set("to", to.toISOString());
    url.searchParams.set("limit", "100");
    for (const s of ["auto_fixed", "blocked", "warned", "passed"]) {
      url.searchParams.append("status", s);
    }
    // Optional documented filters:
    // if (process.env.AGENT_ID) url.searchParams.append("agent_id", process.env.AGENT_ID);
    // if (process.env.RULE_ID) url.searchParams.append("rule_id", process.env.RULE_ID);
    // url.searchParams.append("source", "api");
    // url.searchParams.set("search", "guarantees");
    if (cursor) url.searchParams.set("cursor", cursor);

    const r = await fetch(url, {
      headers: { "x-api-key": process.env.ZERODRIFT_API_KEY! },
    });
    if (!r.ok) throw new Error(`ZeroDrift ${r.status}`);
    const page = await r.json();
    for (const a of page.items as ActivityItem[]) {
      byId.set(a.activity_id, {
        activity_id: a.activity_id,
        occurred_at: a.occurred_at,
        event_name: a.event_name,
        status: a.status,
        source: a.source,
        provider: a.provider,
        model: a.model,
        agent_id: a.agent?.id,
        agent_name: a.agent?.name,
        rule_count: a.rule_count,
        rules: (a.rules ?? []).map((x) => x.name ?? x.id ?? "").join("; "),
      });
    }
    cursor = page.next_cursor ?? null;
  } while (cursor);

  // NDJSON-style lines for warehouses; or convert to CSV in your job runner
  return [...byId.values()].map((row) => JSON.stringify(row)).join("\n");
}

console.log(await exportActivity(1));
```

## Gotchas

* **30 days.** Ranges longer than 30 days return `400`. Export nightly and keep your own copy for longer retention ([Data Retention](https://docs.zerodrift.com/data-retention.md)).
* **Lookback.** Activity is eventually consistent. Overlap the previous window and dedupe by `activity_id` so late records are not dropped. If the overlap would exceed 30 days, clamp `from` to the cap.
* **Filters.** Use only the documented list params above. Optional `metadata` on enforce is merged into the job; do not assume undocumented metadata keys are Activity filters.
* **Tune thresholds.** Compare `auto_fixed` volume to human review outcomes; adjust the confidence threshold in [Act on Each Verdict](https://docs.zerodrift.com/guides/act-on-verdicts.md).

## Next

> **[Data Retention](https://docs.zerodrift.com/data-retention.md)**
>
> How long ZeroDrift keeps content, verdicts, and activity records.

> **[Act on Each Verdict](https://docs.zerodrift.com/guides/act-on-verdicts.md)**
>
> The Pass / Fix / Block / Escalate handler this export helps you tune.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.