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.
List contacts
Section titled “List contacts”GET /api/v1/contacts returns the directory, newest first, cursor-paginated. Scope required: contacts:read.
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_nameis the name your sends addressed them by. Address a recipient asJane Doe <jane@example.com>and the directory learns “Jane Doe”; a bare address leaves whatever name it already had.localeandtimezoneare 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_countcounts every send to that address;sandbox_message_countis the share that never left the sandbox, so you can always tell what a person actually received.propertiesis thedatapayload of the most recent send, kept as personalization context. It is overwritten each time, not merged.
Read one contact
Section titled “Read one contact”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.
curl https://pufferpost.com/api/v1/contacts/con_01J9Z6P2R4T6V8X0Z2B4D6F8H0 \ -H "Authorization: Bearer key_live_…"A contact’s message history
Section titled “A contact’s message history”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.
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.
Answering an access request
Section titled “Answering an access request”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.
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.
Answering an erasure request
Section titled “Answering an erasure 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.
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
datapayload, 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.