Skip to content

Contacts & privacy

Every send writes to a recipient directory: one row per address you have emailed from a workspace, with what the platform learned about them along the way. It is a record of what you sent, not a marketing list. Nothing subscribes anyone, and a contact is never a sending target on its own.

The same data is what a privacy request is answered from, so the two live together here.

All endpoints use the base URL https://pufferpost.com/api/v1 and bearer API-key auth. See /reference/api/ for the full reference.

GET /api/v1/contacts returns the directory, newest first, cursor-paginated. Scope required: contacts:read.

Terminal window
curl "https://pufferpost.com/api/v1/contacts?limit=25" \
-H "Authorization: Bearer key_live_…"
{
"data": [
{
"id": "con_01J9Z6P2R4T6V8X0Z2B4D6F8H0",
"email": "jane@example.com",
"display_name": "Jane Doe",
"locale": "nl",
"timezone": "Europe/Amsterdam",
"message_count": 12,
"sandbox_message_count": 2,
"last_message_at": "2026-09-17T08:41:09+00:00",
"properties": { "plan": "scale" },
"created_at": "2026-04-02T11:00:00+00:00"
}
],
"next_cursor": "eyJpZCI6…"
}

limit is 1–100 and defaults to 25. Pass the previous response’s next_cursor as ?cursor= for the next page; null means you have reached the end.

What fills each field:

  • display_name is the name your sends addressed them by. Address a recipient as Jane Doe <jane@example.com> and the directory learns “Jane Doe”; a bare address leaves whatever name it already had.
  • locale and timezone are the last per-send values you supplied. They are also the recipient preference used by later sends that omit them, which is how a contact keeps rendering in their own language and clock. See Locale and timezone.
  • message_count counts every send to that address; sandbox_message_count is the share that never left the sandbox, so you can always tell what a person actually received.
  • properties is the data payload of the most recent send, kept as personalization context. It is overwritten each time, not merged.

GET /api/v1/contacts/{id} returns a single contact. An id outside your workspace is a 404 with code contact_not_found, never a cross-tenant read.

Terminal window
curl https://pufferpost.com/api/v1/contacts/con_01J9Z6P2R4T6V8X0Z2B4D6F8H0 \
-H "Authorization: Bearer key_live_…"

GET /api/v1/contacts/{id}/messages lists every message sent to that contact, newest first, in the same shape as GET /messages and with the same cursor paging. Scope required: contacts:read.

Terminal window
curl "https://pufferpost.com/api/v1/contacts/con_01J9Z6P2R4T6V8X0Z2B4D6F8H0/messages?limit=10" \
-H "Authorization: Bearer key_live_…"

This is the “what have we sent this person?” question answered in one call, which is usually what a support conversation actually needs.

POST /api/v1/recipients/export returns everything the workspace holds about one address: every message sent to them, each with its full event timeline. Scope required: recipients:read.

Terminal window
curl -X POST https://pufferpost.com/api/v1/recipients/export \
-H "Authorization: Bearer key_live_…" \
-H "Content-Type: application/json" \
-d '{ "recipient": "jane@example.com" }'
{
"recipient": "jane@example.com",
"messages": [
{
"id": "msg_01J9Z2K3M4N5P6Q7R8S9T0V1W2",
"status": "delivered",
"to": "jane@example.com",
"from": "no-reply@acme.com",
"from_name": "Acme Receipts",
"subject": "Your order is confirmed",
"data": { "order_id": "A-1001" },
"events": [
{ "id": "evt_01J9Z8K3QF7M2YB4N6T0XR5VWA", "type": "delivered", "occurred_at": "2026-06-21T09:14:10+00:00" }
]
}
]
}

Addressed to an address you never emailed, it returns an empty messages list rather than a 404: “we hold nothing about this person” is a valid answer to an access request.

POST /api/v1/recipients/erase redacts the recipient across the workspace and reports how many messages it touched. Scope required: recipients:erase. This cannot be undone.

Terminal window
curl -X POST https://pufferpost.com/api/v1/recipients/erase \
-H "Authorization: Bearer key_live_…" \
-H "Content-Type: application/json" \
-d '{ "recipient": "jane@example.com" }'
{ "erased": 12 }

What erasure removes, in one pass:

  • On every message to them: the recipient address and name, the rendered subject, the data payload, metadata, attachments, custom headers, and the cc/bcc lists.
  • On those messages’ timeline events: the stored provider payload, which echoes the address back.
  • On messages addressed to someone else: the address is scrubbed out of cc, bcc and reply-to, without touching the other recipient’s data.
  • The contact directory entry, and the stored body of any webhook delivery that carried the address.

Events themselves survive as types and timestamps, so delivery statistics stay honest while the person behind them is gone.

One refusal to handle: if the account is under a legal hold, erasure is rejected with 409 and code legal_hold. A hold is set by us, for example while a dispute or investigation is open, and it outranks an erasure request until it is lifted.