Domains & senders
Before PufferPost will deliver a message, two things must be true:
- The domain in your from-address is DKIM-verified — you have published the DNS records PufferPost hands you.
- The sender (the exact from-address) is registered and approved out of its initial
under_reviewstate.
This guide walks through both. All requests use your live API key (Authorization: Bearer key_live_…) against the base URL https://app.pufferpost.com/v1. See Authentication for keys and scopes, and the API reference for the full endpoint list.
1. Add a sending domain
POST /v1/domains starts DKIM verification for a domain. It is idempotent on (workspace, domain) — calling it again for the same domain returns the existing record and its current DNS tokens rather than creating a duplicate.
Requires the domains:manage scope.
curl -X POST https://app.pufferpost.com/v1/domains \ -H "Authorization: Bearer key_live_…" \ -H "Content-Type: application/json" \ -d '{ "domain": "mail.acme.com" }'The only field is domain, which must be a syntactically valid domain name (lowercased and trimmed for you). The response is 201 Created:
{ "id": "dom_01J9Z3K7Q2N8M4V6XB0R5T9AHC", "domain": "mail.acme.com", "verified": false, "dkim_status": "pending", "dmarc_status": "unknown", "dns_records": [ { "purpose": "DKIM", "type": "TXT", "host": "mlr1._domainkey.mail.acme.com", "value": "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ" }, { "purpose": "SPF", "type": "TXT", "host": "mail.acme.com", "value": "v=spf1 mx ~all" }, { "purpose": "DMARC", "type": "TXT", "host": "_dmarc.mail.acme.com", "value": "v=DMARC1; p=none;" } ], "created_at": "2026-06-09T10:15:00+00:00"}The DNS records you publish
dns_records is the to-do list for your DNS provider. Every host and value references only your own domain — PufferPost is white-label, so nothing in the records you publish names the platform.
- DKIM (
TXT) — the gate. Publish the public-key record at<selector>._domainkey.<domain>with valuev=DKIM1; k=rsa; p=<public-key>. This is what flipsdkim_statustoverifiedandverifiedtotrue. The DKIM record only appears once a signing key has been issued for the domain. - SPF (
TXT) — for an optional custom MAIL FROM, publish the SPF record atmail.<domain>(alongside its MX record) authorizing the sending source. - DMARC (
TXT) — a recommended starter policy (p=none;) at_dmarc.<domain>. DMARC is advisory: PufferPost observes it but does not block sending on it.
Field meanings:
| Field | Meaning |
|---|---|
verified | true once DKIM is verified — the single signal that the domain may be sent from. |
dkim_status | pending, verified, or failed. |
dmarc_status | unknown, present, or missing (advisory only). |
2. List your domains
GET /v1/domains returns every domain in the workspace with its current status. Requires domains:read.
curl https://app.pufferpost.com/v1/domains \ -H "Authorization: Bearer key_live_…"{ "data": [ { "id": "dom_01J9Z3K7Q2N8M4V6XB0R5T9AHC", "domain": "mail.acme.com", "verified": false, "dkim_status": "pending", "dmarc_status": "unknown", "dns_records": [ "…" ], "created_at": "2026-06-09T10:15:00+00:00" } ]}3. Re-check verification
DNS takes time to propagate. After you have published the records, ask PufferPost to re-poll:
POST /v1/domains/{id}/refresh re-checks the DKIM (and DMARC) status for one domain. Requires domains:manage. The {id} is the dom_… id; a domain belonging to another workspace returns a 404.
curl -X POST https://app.pufferpost.com/v1/domains/dom_01J9Z3K7Q2N8M4V6XB0R5T9AHC/refresh \ -H "Authorization: Bearer key_live_…" \ -H "Content-Type: application/json" \ -d '{}'Once your DKIM TXT resolves, the refreshed record reflects it:
{ "id": "dom_01J9Z3K7Q2N8M4V6XB0R5T9AHC", "domain": "mail.acme.com", "verified": true, "dkim_status": "verified", "dmarc_status": "present", "dns_records": [ "…" ], "created_at": "2026-06-09T10:15:00+00:00"}Refresh as a habit, not just once: PufferPost treats DKIM as a drift check, so a domain whose record is later removed can fall out of verified.
If the id is unknown to your workspace you get a 404:
{ "error": { "code": "domain_not_found", "message": "No such domain." } }4. Register a sender
A sender is the exact from-address a message is sent as. Register one with POST /v1/senders (requires senders:manage). The from-address must be unique within the workspace.
curl -X POST https://app.pufferpost.com/v1/senders \ -H "Authorization: Bearer key_live_…" \ -H "Content-Type: application/json" \ -d '{ "fromAddress": "receipts@mail.acme.com", "displayName": "Acme Receipts" }'fromAddress(required) — a valid email address; normalized to lowercase.displayName(optional) — up to 255 characters; may be omitted ornull.
The response is 201 Created. Note the state:
{ "id": "snd_01J9Z4P2C8H3M0K7V5XB9R2TFE", "workspace_id": "wsp_01J9Z0A1B2C3D4E5F6G7H8J9K0", "from_address": "receipts@mail.acme.com", "display_name": "Acme Receipts", "state": "under_review", "created_at": "2026-06-09T10:20:00+00:00"}If a sender for that address already exists you get a 409:
{ "error": { "code": "sender_exists", "message": "A sender for \"receipts@mail.acme.com\" already exists." } }The review gate
Every new sender starts in state: "under_review". The sender lifecycle is:
| State | Can send? | Meaning |
|---|---|---|
under_review | No | Initial state — awaiting approval. |
active | Yes | Approved; the only state that may send. |
paused | No | Auto-paused by bounce/complaint or abuse thresholds. |
Approval is a platform-side review step, not a self-service API call — there is no public endpoint to approve your own sender. Once review completes, the sender moves to active.
5. Read sender status
GET /v1/senders lists the workspace’s senders; GET /v1/senders/{id} fetches one by its snd_… id (a foreign id is a 404). Both require senders:read.
curl https://app.pufferpost.com/v1/senders/snd_01J9Z4P2C8H3M0K7V5XB9R2TFE \ -H "Authorization: Bearer key_live_…"Poll the state field until it reads active.
6. Now you can send
Sending is gated on both halves being in place. When you call the send API, PufferPost looks up an active sender matching the from-address. If none is found — because the sender is still under_review, was paused, or was never registered — the message is rejected:
{ "error": { "code": "sender_not_available", "message": "No active sender for \"receipts@mail.acme.com\"." } }So the full checklist before your first successful send is:
POST /v1/domainsand publish the returned DKIM record.POST /v1/domains/{id}/refreshuntilverifiedistrue.POST /v1/sendersfor your from-address.- Wait for the sender to reach
state: "active".
With all four done, head to the send guide to deliver your first message.
Gotchas
- DKIM is the gate, not SPF or DMARC.
verifiedflips only on DKIM; DMARC is observed but never blocks. A green SPF record alone will not let you send. - The DKIM record may not be in the first response. It appears once a signing key is issued; if
dns_recordslacks aDKIMentry, refresh and re-read. - From-address must match exactly. The active sender is matched on the normalized (lowercased) from-address.
Receipts@mail.acme.comandreceipts@mail.acme.comare the same sender, butnoreply@…andreceipts@…are not. - Registering a sender does not verify the domain. They are independent steps — a sender on an unverified domain stays unsendable even once approved, and a verified domain does not auto-create senders.
- Everything is workspace-scoped. Domains and senders belonging to another workspace return
404, not403.