Concepts
What a key reaches, sender domains and their DNS (SPF, DKIM, DMARC), API keys and scopes, suppressions, bounces, warmup and daily caps, paused sending, data retention.
What a key reaches
An API key acts on one group of sender domains — the domains it may send from, the messages it may read, the suppression list and the webhooks it may change, and the people who can reach them. Anything outside that group answers 404, as if it didn't exist.
The account owns its domains, keys, members, suppression list and webhooks. A key can be restricted to one domain; this does not grant access to other domains. Historical wire values remain compatible — the value project in a suppression's scope, and the error code project_paused.
Sender domains & DNS
You can send only from a domain that is registered and verified. A domain moves from pending to verified once its SPF, DKIM and DMARC records check out; failed and suspended are set by postfly. A group holds up to 50 domains, and a domain belongs to one group only.
SPF
SPF tells receivers which servers may send for your domain. A domain may publish only one SPF record, so postfly doesn't hand you a second one: it reads your current record and gives it back with include:_spf.postfly.app added up front. The domain's details show the record it replaces and how many DNS lookups it costs (receivers allow 10). If your DNS doesn't answer, no value is offered rather than a guess — try again later. SPF passes when your record authorizes postfly's sending servers, which the include does.
DKIM
postfly generates a 2048-bit key for each domain and signs every message with it, as your domain. You publish the public half as a TXT record at postfly._domainkey.yourdomain.com (the selector is postfly); DKIM passes when the published key matches.
DMARC
DMARC tells receivers what to do with mail that fails authentication, and where to send reports. Verification needs a v=DMARC1 record at _dmarc.yourdomain.com. If the domain already has one, the domain page shows that record, marked as published: keep it. postfly never suggests replacing a p=quarantine or p=reject policy with something weaker, and only offers its own record — p=none, with reports to dmarc@ your domain — when the domain has none or the published one can't be read. Because the DKIM signature is made with your domain, it aligns with the visible From:, which is what DMARC checks. Once the reports look clean, you can move to p=quarantine or p=reject. The same goes for DKIM: once your key is published, the domain page shows the record as it stands rather than asking for it again.
MX
The domain page also lists an MX record pointing at mail.postfly.app. It is optional, not part of verification, and never a replacement: an MX routes all mail addressed to the domain, so if the domain already receives mail elsewhere, keep those records. Publish ours only to receive bounce reports for the domain at postfly — on a subdomain if your own mail stays where it is.
Checks
Re-check now (or POST /v1/domains/{domain}/verify) runs the checks at once, reading the domain's own nameservers rather than a cached answer — a record published a minute ago counts, and the page says while other resolvers may still serve the old one. Each check is pass, fail or unknown; unknown means DNS didn't answer and never counts as a failure. A record only the domain's own nameservers serve yet is shown as published, but the domain stays pending until our resolver sees it too. For their first 30 days, pending domains are re-checked every 10 minutes on the day they are added or once any record has passed, otherwise once a day. Verified domains are re-checked daily too: if their records break two checks in a row, you get one email, and another when they recover (Account → Alerts).
API keys & scopes
Keys are created on a domain's Keys & SMTP tab, as pf_live_… or pf_test_… — the prefix labels the environment you use it in; both send real mail. The key is shown once and only its hash is stored. A revoked or paused key answers 401. The dashboard offers two sets of scopes: Sending only (emails:send) and Full access (emails:send, emails:read, domains:write, suppressions:write, webhooks:write). A key can also be restricted to one sender domain: it then sends only from that domain, sees only its messages and cannot register domains.
| Scope | Allows | Endpoints |
|---|---|---|
| emails:send | Send over the API and SMTP, and read back the messages you sent. | POST /v1/emails GET /v1/emails/{id} |
| emails:read | List and read the messages this key can see. | GET /v1/emails GET /v1/emails/{id} |
| domains:read | See sender domains and their DNS records. | GET /v1/domains GET /v1/domains/{domain} |
| domains:write | Register, verify and delete sender domains. Implies domains:read. | POST /v1/domains DELETE /v1/domains/{domain} POST /v1/domains/{domain}/verify |
| suppressions:read | Browse the suppression list this key can see. | GET /v1/suppressions |
| suppressions:write | Add and remove suppressions. Implies suppressions:read. | POST /v1/suppressions DELETE /v1/suppressions/{email} |
| webhooks:write | Part of Full access; webhooks are managed in the dashboard, and no API endpoint needs this scope yet. |
A request without the scope it needs answers 403 insufficient_scope.
Suppressions
postfly won't send to a suppressed address: such a send answers 409 recipient_suppressed (over SMTP, a 550). There are two lists:
- Your list — shared by every domain in the group, so an address blocked on one is blocked on all of them. Addresses land there when a recipient complains (a feedback-loop report, reason
spam_complaint) and when you add them over the API or in the dashboard (unsubscribe,manual, …). Up to 100,000 addresses. - The global list — shared by every customer to protect the sending IPs. A hard bounce puts the address there automatically. You can look an address up (
GET /v1/suppressions?email=…) but not browse the list or remove from it with an API key.
Bounce categories
A bounced message carries its class in bounce.reason and whether it was hard or soft in bounce.category (hard_bounce / soft_bounce); the email.bounced webhook carries the same as category and hard. Hard bounces suppress the address globally.
| Class | Status codes | Hard | Meaning |
|---|---|---|---|
| mailbox_not_found | 5.1.1, 5.1.10 | yes | The mailbox does not exist. |
| domain_not_found | 5.1.2 | yes | The recipient's domain does not exist. |
| policy_block | 5.4.1, 5.7.1, or "policy" in the reply | yes | The receiver refused the message by policy. |
| invalid_recipient | any other 5.x.x | yes | A permanent refusal of the recipient. |
| transient | 4.x.x | no | A temporary failure that outlasted the mail server's retries. |
| unknown | no status code | no | The reply didn't say. |
Warmup & daily caps
Mail from a new sender builds its reputation gradually, so every new domain starts on a warmup schedule, which caps how many messages it may send per day:
| Warmup days | Messages per day |
|---|---|
| 1–7 | 50 |
| 8–14 | 100 |
| 15–21 | 250 |
| 22–28 | 1,000 |
| from day 29 | 10,000 |
An account may send up to 15,000 messages a day across all its domains, unless a different limit is agreed. postfly may also set a fixed daily cap. The dashboard shows today's count against the cap. A day ends at midnight Europe/Bratislava time. Past the cap, sends answer 429 daily_quota_exceeded with Retry-After — see Daily sending quota. This is separate from the per-key rate limits.
Paused sending
Sending can be paused by the owner (on their Account page) or by postfly. While it is paused, new messages are refused with 403 project_paused (over SMTP, 550 5.7.1), and messages already queued are held and go out once it is resumed. Reading messages, domains and suppressions keeps working.
Data retention
A message's content — subject, HTML and text bodies, headers — is cleared 30 days after it reaches a final state (sent, delivered, bounced or failed); the message itself, with its delivery timeline, is deleted 90 days after the same moment. GET /v1/emails/:id keeps answering for a deleted message's id with 404, the same as one that never existed. Attachment bytes are removed as soon as a message leaves the queue, or after 7 days at the latest either way. Suppressions are never removed by age: they keep blocking regardless of how old the bounce or complaint that created them is. A queued or sending message is never touched by any of this.