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

# Export everything about a person

> Fetch what the API holds about one contact: contact card, tickets, messages, attachments, chat sessions and survey answers. Internal notes and live chats are not included. Suited as the basis for a GDPR Article 15 request.

When a person asks what you have stored about them, you can fetch it over the API, apart from [what is not included](#what-is-included). The result is suited as the basis for a GDPR Article 15 request (a subject access request). Your organization reviews the material before handing it out: a ticket can also hold what other people wrote.

You need an [API key](https://docs.replyful.com/api-reference/authentication.md). A read-only key is enough.

## What is included

| Data | Endpoint |
| - | - |
| Contact card: name, email, phone, company, data fields, privacy consent | [`GET /v1/contacts/{id}`](https://docs.replyful.com/api-reference/contacts/retrieve) |
| Tickets the contact is involved in | [`GET /v1/tickets?contactId=`](https://docs.replyful.com/api-reference/tickets/list) |
| Messages, from every channel | [`GET /v1/conversations/{id}/messages`](https://docs.replyful.com/api-reference/conversations/messages) |
| Attachments | [`GET /v1/attachments/{id}/content`](https://docs.replyful.com/api-reference/attachments/content) |
| Chat sessions: page, IP address, browser | [`GET /v1/contacts/{id}/sessions`](https://docs.replyful.com/api-reference/contacts/sessions) |
| Satisfaction survey answers | The `rating` field on each ticket |

Your agents' internal notes and real-time live chats with an agent are not available in the API. Inline images in an email body are not listed as attachments. Erased contacts and tickets are not returned.

## The steps

**1. Find the contact**

[`GET /v1/contacts?email=`](https://docs.replyful.com/api-reference/contacts/list) with the person's email address, or `?externalId=` with your own user ID for them.

**2. Read the contact card**

Each contact in the list is the full card. Fetch it again later with [`GET /v1/contacts/{id}`](https://docs.replyful.com/api-reference/contacts/retrieve).

**3. List the chat sessions**

[`GET /v1/contacts/{id}/sessions`](https://docs.replyful.com/api-reference/contacts/sessions).

**4. List the tickets**

[`GET /v1/tickets?contactId={id}&sort=createdAt`](https://docs.replyful.com/api-reference/tickets/list). You get the tickets where the contact is the requester and the ones where they take part in a conversation. Compare `requester.id` with the contact's ID to tell which. The survey answer is the ticket's `rating`.

**5. Fetch the messages**

[`GET /v1/conversations/{id}/messages`](https://docs.replyful.com/api-reference/conversations/messages) for every entry in each ticket's `conversations`.

**6. Download the attachments**

[`GET /v1/attachments/{id}/content`](https://docs.replyful.com/api-reference/attachments/content) for every entry in each message's `attachments`. Follow the redirect.

Every list is [paginated](https://docs.replyful.com/api-reference/pagination.md). Follow `nextCursor` until `hasMore` is `false`.

## Good to know

* **One email address can match several contacts.** Only a verified address is unique. An address a visitor merely typed creates a separate contact with `email.verified: false`. Export every contact the lookup returns and judge whether it is the same person.
* **A contact has one email address.** A person who wrote from two addresses is two contacts. Look up each address.
* **Use `sort=createdAt` on the ticket list.** With the default sort, a ticket that is updated while you page can be skipped. With `sort=createdAt` a new ticket shows up at the end, and nothing is skipped or repeated.
* **A large export is many calls.** You make one call per conversation and one per file, and each counts toward the [rate limit](https://docs.replyful.com/api-reference/rate-limits.md). On `429`, wait the number of seconds in `Retry-After` and try again.
* **Only your own data.** An ID that is unknown and an ID from another organization behave the same: `404` or an empty list.

## Complete script

Save as `export-contact.mjs` and run `REPLYFUL_API_KEY=rfl_live_... node export-contact.mjs ada@example.com`. It needs Node.js 18 or later. It writes one folder per matching contact, with an `export.json` and the attachments.

```js Node.js theme={null}
import { mkdir, writeFile } from "node:fs/promises";

const API = "https://api.replyful.com";
const headers = { Authorization: `Bearer ${process.env.REPLYFUL_API_KEY}` };

// Calls the API. On 429 it waits as long as Retry-After says, then tries again.
async function call(path, init) {
  for (;;) {
    const res = await fetch(`${API}${path}`, { headers, ...init });
    if (res.status !== 429) {
      return res;
    }
    const seconds = Number(res.headers.get("Retry-After") ?? "1");
    await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
  }
}

// Returns every item of a list by following nextCursor.
async function listAll(path) {
  const items = [];
  let cursor = null;
  do {
    const url = new URL(path, API);
    url.searchParams.set("limit", "100");
    if (cursor) {
      url.searchParams.set("startingAfter", cursor);
    }
    const res = await call(url.pathname + url.search);
    if (!res.ok) {
      throw new Error(`${path}: ${res.status} ${await res.text()}`);
    }
    const page = await res.json();
    items.push(...page.data);
    cursor = page.hasMore ? page.nextCursor : null;
  } while (cursor);
  return items;
}

// Asks the API for a download link, then fetches the file without the API key.
async function download(attachment, dir) {
  const res = await call(`/v1/attachments/${attachment.id}/content`, {
    redirect: "manual",
  });
  const location = res.headers.get("Location");
  if (res.status !== 302 || !location) {
    throw new Error(`Attachment ${attachment.id}: ${res.status}`);
  }
  const file = await fetch(location);
  if (!file.ok) {
    throw new Error(`Attachment ${attachment.id}: download failed`);
  }
  const name = `${attachment.id}-${attachment.filename.replace(/[^\w.-]/g, "_")}`;
  await writeFile(`${dir}/${name}`, Buffer.from(await file.arrayBuffer()));
}

async function exportContact(contact) {
  const dir = `export/${contact.id}`;
  await mkdir(`${dir}/attachments`, { recursive: true });

  const sessions = await listAll(`/v1/contacts/${contact.id}/sessions`);

  const tickets = await listAll(
    `/v1/tickets?contactId=${contact.id}&sort=createdAt`,
  );
  for (const ticket of tickets) {
    for (const conversation of ticket.conversations) {
      conversation.messages = await listAll(
        `/v1/conversations/${conversation.id}/messages`,
      );
      for (const message of conversation.messages) {
        for (const attachment of message.attachments) {
          await download(attachment, `${dir}/attachments`);
        }
      }
    }
  }

  await writeFile(
    `${dir}/export.json`,
    JSON.stringify({ contact, sessions, tickets }, null, 2),
  );
  console.log(`${contact.id}: ${tickets.length} tickets, saved to ${dir}`);
}

const email = process.argv[2];
if (!email) {
  throw new Error("Usage: node export-contact.mjs <email>");
}

// Can be more than one contact, see "Good to know".
const contacts = await listAll(`/v1/contacts?email=${encodeURIComponent(email)}`);
for (const contact of contacts) {
  await exportContact(contact);
}
console.log(`Exported ${contacts.length} contact(s).`);
```


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