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://pufferpost.com/api/v1. See Authentication for keys and scopes, and the API reference for the full endpoint list.
1. Add a sending domain
Section titled “1. Add a sending domain”POST /api/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://pufferpost.com/api/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"}Two refusals to expect here:
403 domain_limit_reachedwhen the plan’s sending-domain allowance is used up. The message names the allowance; upgrading raises it.429if you add domains in quick succession. Adding a domain provisions DKIM keys, so the rate is capped per account; wait and retry.
The DNS records you publish
Section titled “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
Section titled “2. List your domains”GET /api/v1/domains returns every domain in the workspace with its current status. Requires domains:read.
curl https://pufferpost.com/api/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
Section titled “3. Re-check verification”DNS takes time to propagate. After you have published the records, ask PufferPost to re-poll:
POST /api/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://pufferpost.com/api/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
Section titled “4. Register a sender”A sender is the exact from-address a message is sent as. Register one with POST /api/v1/senders (requires senders:manage). The from-address must be unique within the workspace.
curl -X POST https://pufferpost.com/api/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
Section titled “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
Section titled “5. Read sender status”GET /api/v1/senders lists the workspace’s senders; GET /api/v1/senders/{id} fetches one by its snd_… id (a foreign id is a 404). Both require senders:read.
curl https://pufferpost.com/api/v1/senders/snd_01J9Z4P2C8H3M0K7V5XB9R2TFE \ -H "Authorization: Bearer key_live_…"Poll the state field until it reads active.
6. Now you can send
Section titled “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, whether 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 /api/v1/domainsand publish the returned DKIM record.POST /api/v1/domains/{id}/refreshuntilverifiedistrue.POST /api/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
Section titled “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.