Concepts
Projects, sender domains and their DNS (SPF, DKIM, DMARC), API keys and scopes, suppressions, bounces, warmup and daily caps, paused projects.
Projects
Everything in postfly belongs to a project: its sender domains, API keys, messages, suppressions, webhooks and team. An API key acts for exactly one project. Anything that belongs to another project answers 404, as if it didn't exist.
Sender domains & DNS
You can send only from a domain the project has 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 project holds up to 50 domains, and a domain can be registered in one project 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. Pending domains are re-checked once a day for their first 30 days. Verified domains are re-checked daily too: if their records break two checks in a row, the project gets one email, and another when they recover (Settings → Notifications).
API keys & scopes
Keys are created in API tokens, one project each, 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 project's messages. | 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 project's suppression list. | GET /v1/suppressions |
| suppressions:write | Add and remove project 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:
- The project list — yours. 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 all projects 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 postfly can put a project on a warmup schedule, which caps how many messages it accepts per day:
| Warmup days | Messages per day |
|---|---|
| 1–7 | 50 |
| 8–14 | 100 |
| 15–21 | 250 |
| 22–28 | 1,000 |
| from day 29 | no cap |
postfly may also set a fixed daily cap for a project. 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 projects
A project can be paused by its owner (Settings) 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.