Skip to content
Console
Browse documentation
Guide

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.

On this page

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, 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 and the Console guide. 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 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, 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.

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.

POST /api/v1/projects/{projectId}/person-exports
{
  "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:

{
  "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

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

{
  "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

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:

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