Concepts

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 (SettingsNotifications).

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.

ScopeAllowsEndpoints
emails:sendSend over the API and SMTP, and read back the messages you sent.
POST /v1/emails
GET /v1/emails/{id}
emails:readList and read the project's messages.
GET /v1/emails
GET /v1/emails/{id}
domains:readSee sender domains and their DNS records.
GET /v1/domains
GET /v1/domains/{domain}
domains:writeRegister, verify and delete sender domains. Implies domains:read.
POST /v1/domains
DELETE /v1/domains/{domain}
POST /v1/domains/{domain}/verify
suppressions:readBrowse the project's suppression list.
GET /v1/suppressions
suppressions:writeAdd and remove project suppressions. Implies suppressions:read.
POST /v1/suppressions
DELETE /v1/suppressions/{email}
webhooks:writePart 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.

ClassStatus codesHardMeaning
mailbox_not_found5.1.1, 5.1.10yesThe mailbox does not exist.
domain_not_found5.1.2yesThe recipient's domain does not exist.
policy_block5.4.1, 5.7.1, or "policy" in the replyyesThe receiver refused the message by policy.
invalid_recipientany other 5.x.xyesA permanent refusal of the recipient.
transient4.x.xnoA temporary failure that outlasted the mail server's retries.
unknownno status codenoThe 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 daysMessages per day
1750
814100
1521250
22281,000
from day 29no 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.

The API reference lists the scope and rate-limit bucket of every endpoint.