# Export a person's data

> Get one archive of what is held about one person, including recorded agent session replays and evaluations of their own whole runs, with a manifest that says exactly what is in it, how fresh it is and what is not in it.

Canonical page: https://anectico.com/docs/manage/export-a-person/


Exporting a person answers a request for access: someone asks what you hold about them, and you
hand them a file. It changes nothing about the person. Only an owner or administrator can do it,
and who asked is recorded.

It is the counterpart of [erasing a person](/docs/manage/erase-a-person), and it reaches the same
person the same way: you name them by their person id or by any one of their identifiers, and the
export covers every identifier linked to them.

You start an export in the Console: open **Privacy** (`/privacy`), stay on the **Requests** tab, and
find the person first. See [Request an export](#request-an-export) and the
[Console guide](/docs/manage/console#privacy). Your agent can request and read the same export with
the MCP tools or the CLI commands in that section.

## What you get

One zip archive. Inside it:

- one file per kind of record, named `<kind>.ndjson`. Each line is one JSON object, so the file can
  be read a line at a time by any tool;
- `manifest.json`, which lists every kind with its number of records, the time range they cover and
  the moment the kind is complete as of, says how recent the archive's data is, and lists what the
  archive does **not** contain and why.

Every kind has a file, including the ones with no records: an empty file says "there were none",
which a missing file could not.

| File | What it holds |
| --- | --- |
| `person.ndjson` | The person's profile: when they were first seen, whether they are identified, and their current properties (everything your `identify()` calls attached, such as email, name and plan). |
| `identifiers.ndjson` | Every identifier linked to the person, including the anonymous ones from before they signed in, with when each was linked. |
| `identity_history.ndjson` | When each identifier was linked, moved or merged. |
| `person_property_history.ndjson` | Every recorded change to the person's properties. |
| `product_events.ndjson` | The product events captured under any of the person's identifiers, such as page views and the custom events you send, with their properties. |
| `errors.ndjson` | The error occurrences recorded under any of the person's identifiers. |
| `diagnostic_captures.ndjson` | Diagnostic sampling requests that name the person: their limits, status and times. Captured telemetry is in the error, log and span files. |
| `diagnostic_grants.ndjson` | Retained sampling grants issued under the person's identifiers, with their budgets, acknowledgement and expiry. |
| `fix_subjects.ndjson` | Fix declarations that named this person, with their own affected operations and declaration metadata. Other people, shared history and the operator note are excluded. |
| `problem_reports.ndjson` | The person's submitted problem reports, including their text and route when the project's export content policy permits them. |
| `logs.ndjson` | The log lines recorded under any of the person's identifiers. |
| `spans.ndjson` | The trace spans recorded under any of the person's identifiers, with their events. |
| `agent_events.ndjson` | The agent events recorded under any of the person's identifiers. |
| `agent_session_replays.ndjson` | Recorded replays of the person's agent sessions: replay identity, outcome, progress and times, with ordered turns, comparisons, the person's words and agent replies when your export content policy permits them. Exact reply bytes are base64 in `reply_json_base64`. |
| `cohort_memberships.ndjson` | The cohorts the person is in, including the earlier versions of a cohort's member list that are kept as history. |
| `experiment_assignments.ndjson` | The experiments the person was assigned to, and which arm. |
| `session_recordings.ndjson` | The session recordings made under any of the person's identifiers: when each started and ended and how large it is. |
| `survey_receipts.ndjson` | Which surveys the person was shown, answered or dismissed, the revision and when; caller-chosen display, response or dismissal identifiers when the project's export content policy permits them. Their answers are in `product_events.ndjson`. |
| `survey_offers.ndjson` | Which surveys the person was offered, the revision and when. |
| `evaluation_jobs.ndjson` | Evaluation attempts about the person's own runs, their status, verdicts and execution details; references to the evaluator and experiment, without their shared definitions. |
| `evaluation_dataset_items.ndjson` | Cases frozen from the person's own runs, with their promotion and review record and content your export policy permits. |
| `evaluation_trials.ndjson` | Experiment trials about the person's own runs or their frozen cases; references to candidates, without shared configurations. |
| `evaluation_annotations.ndjson` | Reviewers' judgments about the person's own runs and frozen cases; comments your export policy permits. |
| `evaluation_results.ndjson` | Quality scores about the person's own runs and frozen cases, with evaluation provenance and explanations your export policy permits. |
| `group_memberships.ndjson` | The accounts (groups) the person's identifiers are members of, including memberships that have ended: the account's type and key, whether the membership is current, and when it began and last changed. |
| `group_membership_history.ndjson` | When each of those account memberships began, ended or moved to another account. |

A run or trace belongs wholly to this person when a retained, visible span or agent event names
one of their current identifiers, or it belongs to their own session or turn, and every such
record that names somebody names one of those identifiers. Their whole runs include records that
name nobody, such as child spans; a session or turn with a trace naming someone else is shared.
When someone else's only identifying row was already erased, that hidden row no longer makes the
remaining run shared. Shared runs contribute only records carrying this person's identifiers.

## How fresh an archive is

Data you send does not become readable the instant it is accepted: it takes a moment to be
stored, usually a few seconds and up to about a minute when little is being sent. An export
that read the person's data the moment you asked could miss what they did just before. So an
export waits before it reads, and then tells you exactly how fresh it is.

**The wait.** Every export waits 60 seconds after it is requested. That is longer than data that
was already being written at that moment normally takes to be stored. It is not a guarantee: a
write that fails is retried and takes longer, and data that had been accepted but was still
queued, not yet being written, is not covered by the 60 seconds at all. After that the export
checks whether the product events and identity updates your project had sent before the request
have been stored. If some have not, or the check cannot be made, it tries again every few seconds
and reads as soon as they have. It stops waiting three minutes after the request whatever it
finds, and the archive is then built as soon as the platform gets to it: normally within seconds,
later when several exports are being built at once. While it waits, the export's state is
`PERSON_EXPORT_STATE_WAITING_FOR_RECENT_DATA` and `waiting_until` says when it will next be looked
at. Nothing is wrong and there is nothing to do.

**"Complete as of".** Every kind of record that was read carries `complete_as_of`, on the export
and in the manifest. It means: this file contains every record of this kind that was stored at
that moment under the identifiers and whole runs the archive was built from. A record that was stored later may
be missing. Telemetry and evaluations of whole runs share one moment, taken before deciding which runs
belong wholly to the person. Neither that decision nor those files include records accepted
after it. A record accepted earlier can finish processing during the build: the set of whole runs
stays fixed, but the archive is not a frozen copy of storage at that moment. Other kinds take their
own moment just before reading. Erasure and retention still
remove records, and mutable evaluation status and review details are their current values; this
is not a historical snapshot. The earliest
`complete_as_of` across the kinds is the moment the whole archive is complete as of; the Console
shows that one.

Two limits come with it, and the export states both:

- **The identifiers are read once.** An export first reads which identifiers are the person's, and
  records and whole runs in the archive are then found by those identifiers. `identifiers_read_at`, on the
  export and in the manifest, is when that was. An identifier that became the person's after it,
  for example an anonymous visitor who signed in while the archive was being built, is not
  covered, and neither are the records stored under it. Request a new export to include them.
- **A kind with nothing to read has no moment.** A kind is marked `nothing_to_read` when the
  person has no identifier that kind of record is stored under, so there was nothing to ask for.
  Its file is empty and it has no `complete_as_of`. That is different from an empty file with a
  `complete_as_of`, which says the records were looked for at that moment and there were none.

**What was still on its way.** `freshness` on the export, and the same section of the manifest,
says how long the export waited and what it found. What it found is always about one period:
the product events and identity updates your project sent between `capture_accepted_from` and
`capture_accepted_through`. `capture_accepted_through` is the request. `capture_accepted_from` is
how far back the check reached: up to twenty-four hours before the request, and at least one.
**Nothing sent before `capture_accepted_from` was checked**, and no state says anything about it.

| `capture_state` | Meaning |
| --- | --- |
| `PERSON_EXPORT_CAPTURE_STATE_SETTLED` | Of what your project sent in that period, nothing was still waiting to be stored at `capture_checked_at`, before the archive was read. |
| `PERSON_EXPORT_CAPTURE_STATE_PENDING` | The three minutes ran out with some still waiting to be stored. `capture_pending` says how many, and `capture_oldest_pending_accepted_at` when the oldest of them was sent. If the person was active in that time, request a new export. |
| `PERSON_EXPORT_CAPTURE_STATE_UNKNOWN` | The check could not be made before the three minutes ran out. `capture_unknown_reason` says why: `record_unavailable` (it could not be read) or `check_incomplete` (it answered, but not for a period that can be stated). The export waited and claims nothing more than each kind's `complete_as_of`. |

Every one of these counts is of your whole project, not of this person: the check cannot tell
whose an event is.

`capture_not_stored` is a separate count for the same period: events that were not stored and
that the export did not wait for, because waiting could not store them. An event is counted here
when it was set aside as unprocessable after it was accepted, or when the platform never
confirmed handing it on for storage (the request that sent it was told so, or was cut off). If
any of them were this person's, they are not in the archive. It is normally `0`.

That check covers product events and identity updates. For errors, logs, trace spans and agent
events there is no such check, and the 60-second wait is all there is: one that was accepted
before the request and took longer than that to be stored is not in the archive. If your
telemetry was arriving late when you asked, request the export again later.

Nothing sent **after** the request is promised. An export is a record as of a moment, and it says
which.

## What it does not contain

Read this before you tell someone the archive is everything. The archive says the same in its
manifest, so the file you hand over is honest by itself.

- **Identity-less records and evaluations of shared runs.** A trace or agent session that names
  someone else contributes only records carrying this person's identifiers. Records that name
  nobody in a shared run, and evaluations of that shared activity, describe others too and are
  left out. Records that name nobody in this person's whole runs are included.
- **Shared evaluation definitions and delivery copies.** Evaluator, dataset, experiment and
  candidate configurations describe the customer's setup across many runs. The archive holds the
  references and the attempts, cases, trials, judgments and results about this person's runs;
  delivery copies of those results add no personal fact.
- **The content of session recordings.** The archive lists each recording. The recording itself is
  not in the archive; a replay link from your agent opens it on a read-only proof page.
- **Content your project's content policy keeps out of exports.** If your
  [content policy](/docs/manage/content-policy) does not allow a class of recorded content to be
  exported, it is left out of this export too, and the manifest names it. A project that has not
  changed its policy exports model prompts, completions and tool call arguments, and does **not**
  export the person's property bag: `person.ndjson` then carries the profile without its
  properties and says they were withheld, and `person_property_history.ndjson` is empty. Allow
  person properties at the export boundary in your content policy, then request a new export, to
  include them.
- **The properties of the accounts the person belongs to.** The archive lists the person's account
  memberships. An account's properties describe the account and every member of it, not the
  person.
- **The person's rows inside analytics results that were already measured.** A measured result
  keeps working copies of what it counted until the result expires. Each copies a product event,
  with the number or value the analysis read from it; the events themselves are in
  `product_events.ndjson`. Where an analysis grouped people by a profile property, the copy also
  holds the value that property had when the analysis ran, for the person or for one of their
  accounts. The person's properties and their recorded changes are in `person.ndjson` and
  `person_property_history.ndjson` when your content policy allows person properties to be
  exported; an account's properties are not exported (see above).
- **Records that were refused when they were sent.** A refused record is kept as raw bytes for
  seven days so the refusal can be diagnosed. It was never accepted as a record about anyone, and
  it cannot be picked out by person without the rest of the batch it was sent in.
- **Not yet part of the export:** quality scores,
  review notes and evaluation dataset items about the person's agent runs; and action receipts
  that name them.
- **The audit log and the records of earlier deletions made for the person.** They record what
  your organization's operators did, not data collected from the person.

## Who can export

The `persons:export` permission, which owners and administrators hold. An API key can be given it
explicitly; a project-scoped key is confined to its own project.

`persons:export` is not enough by itself. The credential must also hold every permission that
reads the person's data: `persons:read`, `persons:profile:read`, `analytics:read`, `errors:read`,
`logs:read`, `traces:read`, `agents:read`, `agents:content:read`, `experiments:read`, `groups:read`,
`replay:read`, `evals:read`, `scores:read` and `surveys:read`. A credential missing one is refused with a message that names it. That is
deliberate: an archive built for someone who could not read the logs would be an archive without
logs, and nothing about the file would say so.

Survey receipt identifiers require `surveys:read` and `agents:content:read`, and
the project's export policy must permit both `model_transcript` and
`tool_arguments`, just as for survey answers. A denied or unreadable policy
withholds the identifiers and names them with the usual content-withheld marker.
The response or dismissal outcome and its time remain visible. Survey offers
have no caller-chosen display, response or dismissal identifier.

Recorded replay text requires your export policy to permit `model_transcript`.
Opaque reply bytes and caller-chosen session, turn, run and requester identifiers
also require `tool_arguments`, because they can carry other content. A denied or
unreadable policy withholds those values and names the fields in the row and in
the manifest; replay outcome, counts and times remain visible. Response digests
and internal deletion or delivery bookkeeping are left out because they add no
recorded words or outcome. Replays that resolved no person are not included.

The same permissions are checked again every time the archive is read or downloaded. Removing one
from a credential stops it downloading archives that were already made.

## Request an export

An export is of one person in one project. It is built in the background: the request returns at
once with the export **pending**, and the export becomes **complete** when the archive is stored.
That takes a little over a minute, because the export first
[waits for recently sent data](#how-fresh-an-archive-is), and longer when there is a lot to read,
data is still being stored, or other exports are being built.

**In the Console.** Open **Privacy** (`/privacy`) and stay on the **Requests** tab. In the **A person**
card, enter the person's id or any one of their identifiers and choose **Find person**. The page
looks the person up first, and you can start an export only for a person it finds. Choose **Export
this person's data**. The dialog shows the export waiting for recent data, then being built, and then
what the archive holds, the moment it is complete as of, what it does not hold, and a **Download
archive** button.

**From the command line.**

```bash
anectico persons export user@example.com
anectico persons exports get <export-id>
anectico persons exports download <export-id> --file person.zip
```

`persons export` prints an export id before it sends the request. If the request is interrupted,
pass that id back with `--export-id` to retry the same request rather than start a second one.

**From an agent.** `request_person_export` previews whose data the export covers and asks for
confirmation; `get_person_export` reads it; `get_person_export_download` returns where the archive
is downloaded. The export has no proof page. The CLI also lists exports with
`anectico persons exports list` and prints the download address with
`anectico persons exports download-url <export-id>`.

**From the API.**

```http
POST /api/v1/projects/{projectId}/person-exports
```

```json
{
  "person": "user@example.com",
  "export_id": "6f3c7c1e-1c0b-4a55-9d5e-1d1f0b1c2a3b"
}
```

`person` is the person's id or any one of their identifiers. It is matched exactly as you send it,
including case and surrounding spaces. It goes in the body, never in the URL.

`export_id` is a UUID you generate. Sending the same id again returns the export that was already
recorded, with `"already_recorded": true`, so a retry never builds a second archive. The same id
for a different person is refused.

The response is the export:

```json
{
  "export": {
    "export_id": "6f3c7c1e-1c0b-4a55-9d5e-1d1f0b1c2a3b",
    "project_id": "…",
    "person_id": "…",
    "requested_by": "user:…",
    "requested_at": "2026-10-05T12:00:00Z",
    "state": "PERSON_EXPORT_STATE_PENDING",
    "kinds": [],
    "not_included": []
  },
  "already_recorded": false
}
```

No response contains one of the person's identifiers. `person_id` is the person's id, and
`distinct_id_count` says how many identifiers the archive was built from.

## Read an export

```http
GET /api/v1/projects/{projectId}/person-exports/{exportId}
GET /api/v1/projects/{projectId}/person-exports?person_id=…&limit=50&cursor=…
```

`state` is one of:

| State | Meaning |
| --- | --- |
| `PERSON_EXPORT_STATE_PENDING` | Recorded, not looked at yet (or about to be tried again after part of the data could not be read). |
| `PERSON_EXPORT_STATE_WAITING_FOR_RECENT_DATA` | Deliberately not built yet: it is [waiting for recently sent data](#how-fresh-an-archive-is) to be stored. `waiting_until` says when it is looked at again. |
| `PERSON_EXPORT_STATE_RUNNING` | Being built. |
| `PERSON_EXPORT_STATE_COMPLETE` | The archive is stored and can be downloaded until `expires_at`. |
| `PERSON_EXPORT_STATE_FAILED` | No archive was stored. `error_code` and `error` say why. |
| `PERSON_EXPORT_STATE_EXPIRED` | The archive was deleted when its retention period ended. |

Once complete, `kinds` lists every file with its `rows` (a decimal string), its `earliest` and
`latest` record times and its `complete_as_of`, or `nothing_to_read`; `identifiers_read_at` is
when the person's identifiers were read; `freshness` says how long the export waited and what was
still being stored, for which period (`waited_seconds`, `settle_window_seconds`, `capture_pending`
and `capture_not_stored` are decimal strings); `not_included` repeats the manifest's list; and
`size_bytes` is the archive's size.

```json
{
  "state": "PERSON_EXPORT_STATE_COMPLETE",
  "kinds": [
    {
      "kind": "product_events",
      "rows": "412",
      "earliest": "2026-09-01T08:00:00Z",
      "latest": "2026-10-05T11:59:48Z",
      "complete_as_of": "2026-10-05T12:01:01.204Z",
      "nothing_to_read": false
    }
  ],
  "identifiers_read_at": "2026-10-05T12:01:01.190Z",
  "freshness": {
    "reading_began_at": "2026-10-05T12:01:01Z",
    "waited_seconds": "61",
    "settle_window_seconds": "60",
    "capture_state": "PERSON_EXPORT_CAPTURE_STATE_SETTLED",
    "capture_checked_at": "2026-10-05T12:01:01.180Z",
    "capture_pending": "0",
    "capture_accepted_from": "2026-10-04T12:00:00Z",
    "capture_accepted_through": "2026-10-05T12:00:00Z",
    "capture_oldest_pending_accepted_at": null,
    "capture_not_stored": "0",
    "capture_unknown_reason": ""
  }
}
```

There is no limit on the number of whole runs selected for an archive. The archive's size,
one record's size and the time available to build it still apply. A build attempt has at most
two hours; if that expires, the export fails without a partial archive. Temporary working
copies of run identifiers are removed after the build, or after an abandoned build's expiry,
and are removed when the person or project is deleted.

A failed export stored nothing. It never stores part of a person:

| `error_code` | What happened | What to do |
| --- | --- | --- |
| `person_not_found` | The person was erased, or otherwise no longer exists, before the archive was built. | Nothing: there is nobody to export. |
| `too_large` | The person's data is larger than one archive may hold. Nothing was truncated. | Contact support. |
| `source_unavailable` | Part of the person's data could not be read after several attempts. | Request the export again. |
| `project_deleted` | The project was deleted before the archive was built. | Nothing: the project's data is being deleted. |
| `export_failed` | Anything else. | Request the export again, or contact support. |

## Download the archive

```http
GET /api/v1/projects/{projectId}/person-exports/{exportId}/download
```

returns `{"url": "…", "expires_at": "…"}`. The URL is an address of this API. Fetch it with the
same credential:

```bash
curl -H "Authorization: Bearer $TOKEN" -o person.zip "$URL"
```

The URL is not a secret and grants nothing by itself: every download is authorized again with the
credential that makes it, and a long download is checked again while it is in progress.
`expires_at` is when the archive is deleted, not when the URL stops working.

## How long an archive is kept

An archive is the person's whole record in one file, so it is not kept longer than it is needed:

- it is deleted automatically seven days after it was built. The export then reads
  `PERSON_EXPORT_STATE_EXPIRED`; request a new one if you still need it;
- it is deleted at once when the person is [erased](/docs/manage/erase-a-person). The erasure does
  not finish until every archive of that person is gone, and an export requested for an erased
  person is refused;
- it is deleted with its project or organization.

A copy you have downloaded is yours to look after. Nothing here can delete it.

## If the person is erased while an export is being built

The export stores nothing. An export that had already finished is deleted with its archive. Either
way, an erasure never leaves an archive behind.
