> ## 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](/api-reference/activities/list-activities):

| 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.

<CodeGroup>
  ```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));
  ```
</CodeGroup>

## Gotchas

* **30 days.** Ranges longer than 30 days return `400`. Export nightly and keep your own copy for longer retention ([Data Retention](/data-retention)).
* **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](/guides/act-on-verdicts).

## Next

<CardGroup cols={2}>
  <Card title="Data Retention" icon="clock" href="/data-retention">
    How long ZeroDrift keeps content, verdicts, and activity records.
  </Card>

  <Card title="Act on Each Verdict" icon="scale-balanced" href="/guides/act-on-verdicts">
    The Pass / Fix / Block / Escalate handler this export helps you tune.
  </Card>
</CardGroup>
