API reference

API reference

Every endpoint of the postfly REST API with its scope, rate-limit bucket, parameters, request body and responses — generated from the OpenAPI description.

Authentication

Base URL https://api.postfly.app. JSON in, JSON out, camelCase fields. Every request carries an API key of the project as a bearer token; each endpoint below names the scope it needs (any one of those listed) and the rate-limit bucket it counts against (default 120/min, dns 20/min per key — see Rate limits).

header
Authorization: Bearer pf_live_…

Pagination

GET /v1/emails and GET /v1/suppressions return newest first, one page at a time: data, hasMore, nextCursor and prevCursor. Pass before=<nextCursor> for the next, older page and after=<prevCursor> to go back towards newer items — not both. hasMore refers to the direction you asked for; limit is 1–100, default 25. Cursors are opaque; a malformed one is 400 invalid_request.

Emails

GET/v1/emails

List the project's messages, newest first

Scope
emails:read
Rate limit
default

Query parameters

FieldTypeDescription
limitinteger1–100 · default 25
beforestringnextCursor of a previous page — continue towards older items. Not with after.
afterstringprevCursor of a previous page — continue towards newer items. Not with before.
statusqueued | sending | sent | delivered | bounced | complained | failed
tostring (email)Recipient address, case-insensitive exact match.
fromstring (email)Sender address, case-insensitive exact match.
tagstringmax 64 characters
sincestring (date-time)Created at or after (inclusive).
untilstring (date-time)Created before (exclusive).

Responses

  • 200 One page of messages.
  • 400 Validation failed (invalid_request, invalid_json, …).
  • 401 Missing, malformed, revoked or paused API key.
  • 403 Missing scope, a restricted key, a paused project or another rule; code says which.
  • 429 The key's rate-limit bucket is used up (rate_limit_exceeded), or, on POST /v1/emails, the project's daily quota (daily_quota_exceeded).

Errors come in the error envelope; the codes are in Error codes.

200 response body

FieldTypeDescription
dataobject[]
data[].idstring
data[].statusqueued | sending | sent | delivered | bounced | complained | failed
data[].fromstring
data[].tostring
data[].subjectstring
data[].tagstring | null
data[].bounceobject | nullNull unless the message bounced.
data[].bounce.reasonstringThe bounce class: transient, mailbox_not_found, domain_not_found, policy_block, invalid_recipient or unknown.
data[].bounce.categoryhard_bounce | soft_bounce | null
data[].smtpResponsestring | null
data[].timestampsobject
data[].timestamps.createdstring (date-time)
data[].timestamps.sentstring (date-time) | null
data[].timestamps.deliveredstring (date-time) | null
data[].timestamps.failedstring (date-time) | null
hasMorebooleanMore items exist in the direction requested.
nextCursorstring | nullPass as before for older items.
prevCursorstring | nullPass as after for newer items.

POST/v1/emails

Queue a transactional email

Scope
emails:send
Rate limit
default

Request body

Either html or text is required.

FieldTypeDescription
fromrequiredstring (email)Address on a verified sender domain of the project.max 254 characters
fromNamestringNo line breaks.max 255 characters
torequiredstring (email)Exactly one recipient.max 254 characters
replyTostring (email)max 254 characters
subjectrequiredstringNo line breaks.min 1 characters · max 998 characters
htmlstringmax 26214400 characters
textstringmax 26214400 characters
headersRecord<string, string>Extra headers; ones postfly composes itself are ignored.
tagstringmax 64 characters · pattern ^[A-Za-z0-9._-]+$

Responses

  • 202 Queued; delivery is asynchronous.
  • 400 Validation failed (invalid_request, invalid_json, …).
  • 401 Missing, malformed, revoked or paused API key.
  • 403 Missing scope, a restricted key, a paused project or another rule; code says which.
  • 409 Conflicts with existing state (domain_taken, recipient_suppressed).
  • 429 The key's rate-limit bucket is used up (rate_limit_exceeded), or, on POST /v1/emails, the project's daily quota (daily_quota_exceeded).

Errors come in the error envelope; the codes are in Error codes.

202 response body

FieldTypeDescription
idstring
status"queued"
createdAtstring (date-time)

GET/v1/emails/{id}

One message and its delivery status

Scope
emails:read or emails:send
Rate limit
default

Path parameters

FieldTypeDescription
idrequiredstringpattern ^m_

Responses

  • 200 The message.
  • 400 Validation failed (invalid_request, invalid_json, …).
  • 401 Missing, malformed, revoked or paused API key.
  • 403 Missing scope, a restricted key, a paused project or another rule; code says which.
  • 404 No such resource in this project.
  • 429 The key's rate-limit bucket is used up (rate_limit_exceeded), or, on POST /v1/emails, the project's daily quota (daily_quota_exceeded).

Errors come in the error envelope; the codes are in Error codes.

200 response body

FieldTypeDescription
idstring
statusqueued | sending | sent | delivered | bounced | complained | failed
fromstring
tostring
subjectstring
tagstring | null
bounceobject | nullNull unless the message bounced.
bounce.reasonstringThe bounce class: transient, mailbox_not_found, domain_not_found, policy_block, invalid_recipient or unknown.
bounce.categoryhard_bounce | soft_bounce | null
smtpResponsestring | null
timestampsobject
timestamps.createdstring (date-time)
timestamps.sentstring (date-time) | null
timestamps.deliveredstring (date-time) | null
timestamps.failedstring (date-time) | null

Sender domains

GET/v1/domains

The project's sender domains (at most 50, not paginated)

Scope
domains:read
Rate limit
default

Responses

  • 200 All domains the key may see.
  • 401 Missing, malformed, revoked or paused API key.
  • 403 Missing scope, a restricted key, a paused project or another rule; code says which.
  • 429 The key's rate-limit bucket is used up (rate_limit_exceeded), or, on POST /v1/emails, the project's daily quota (daily_quota_exceeded).

Errors come in the error envelope; the codes are in Error codes.

200 response body

FieldTypeDescription
dataobject[]
data[].domainstring
data[].statuspending | verified | failed | suspended
data[].verifiedboolean
data[].verificationobjectStored per-record flags from the last conclusive checks.
data[].verification.spfboolean
data[].verification.dkimboolean
data[].verification.dmarcboolean
data[].lastCheckedAtstring (date-time) | null
data[].verifiedAtstring (date-time) | null
data[].createdAtstring (date-time)

POST/v1/domains

Register a sender domain and get the DNS records to publish

Scope
domains:write
Rate limit
dns

Request body

domain, or Resend-style name.

FieldTypeDescription
domainstringmax 253 characters
namestringmax 253 characters

Responses

  • 201 Created (status pending).
  • 400 Validation failed (invalid_request, invalid_json, …).
  • 401 Missing, malformed, revoked or paused API key.
  • 403 Missing scope, a restricted key, a paused project or another rule; code says which.
  • 409 Conflicts with existing state (domain_taken, recipient_suppressed).
  • 429 The key's rate-limit bucket is used up (rate_limit_exceeded), or, on POST /v1/emails, the project's daily quota (daily_quota_exceeded).

Errors come in the error envelope; the codes are in Error codes.

201 response body

FieldTypeDescription
domainstring
statuspending | verified | failed | suspended
verifiedboolean
verificationobjectStored per-record flags from the last conclusive checks.
verification.spfboolean
verification.dkimboolean
verification.dmarcboolean
lastCheckedAtstring (date-time) | null
verifiedAtstring (date-time) | null
createdAtstring (date-time)
recordsobject[]
records[].purposespf | dkim | dmarc | mx
records[].typeTXT | MX
records[].namestringFully qualified record name.
records[].valuestring | nullnull when postfly can't tell what to publish — see warning.
records[].requiredbooleanfalse for the optional MX record.
records[].verifiedboolean | nullnull for MX (not checked).
records[].publishedbooleanThe domain publishes this record already and should keep it: value is what is in DNS now, not a replacement. postfly never suggests weakening a DMARC policy or replacing a DKIM key or MX records that are already right.
records[].replacesstring | nullSPF only: the current record to overwrite (one SPF record per domain).
records[].lookupsinteger | nullSPF only: DNS-lookup terms in value (limit 10).
records[].warningcurrent_record_unavailable | too_many_dns_lookups | nullSPF only.

GET/v1/domains/{domain}

Domain status and the DNS records to publish

Scope
domains:read
Rate limit
dns

Path parameters

FieldTypeDescription
domainrequiredstring

Responses

  • 200 The domain with its records.
  • 401 Missing, malformed, revoked or paused API key.
  • 403 Missing scope, a restricted key, a paused project or another rule; code says which.
  • 404 No such resource in this project.
  • 429 The key's rate-limit bucket is used up (rate_limit_exceeded), or, on POST /v1/emails, the project's daily quota (daily_quota_exceeded).

Errors come in the error envelope; the codes are in Error codes.

200 response body

FieldTypeDescription
domainstring
statuspending | verified | failed | suspended
verifiedboolean
verificationobjectStored per-record flags from the last conclusive checks.
verification.spfboolean
verification.dkimboolean
verification.dmarcboolean
lastCheckedAtstring (date-time) | null
verifiedAtstring (date-time) | null
createdAtstring (date-time)
recordsobject[]
records[].purposespf | dkim | dmarc | mx
records[].typeTXT | MX
records[].namestringFully qualified record name.
records[].valuestring | nullnull when postfly can't tell what to publish — see warning.
records[].requiredbooleanfalse for the optional MX record.
records[].verifiedboolean | nullnull for MX (not checked).
records[].publishedbooleanThe domain publishes this record already and should keep it: value is what is in DNS now, not a replacement. postfly never suggests weakening a DMARC policy or replacing a DKIM key or MX records that are already right.
records[].replacesstring | nullSPF only: the current record to overwrite (one SPF record per domain).
records[].lookupsinteger | nullSPF only: DNS-lookup terms in value (limit 10).
records[].warningcurrent_record_unavailable | too_many_dns_lookups | nullSPF only.

DELETE/v1/domains/{domain}

Remove a sender domain from the project

Scope
domains:write
Rate limit
default

Path parameters

FieldTypeDescription
domainrequiredstring

Responses

  • 200 Deleted.
  • 401 Missing, malformed, revoked or paused API key.
  • 403 Missing scope, a restricted key, a paused project or another rule; code says which.
  • 404 No such resource in this project.
  • 429 The key's rate-limit bucket is used up (rate_limit_exceeded), or, on POST /v1/emails, the project's daily quota (daily_quota_exceeded).

Errors come in the error envelope; the codes are in Error codes.

200 response body

FieldTypeDescription
domainstring
deletedtrue

POST/v1/domains/{domain}/verify

Re-run the SPF / DKIM / DMARC checks now

Scope
domains:write
Rate limit
dns

Path parameters

FieldTypeDescription
domainrequiredstring

Responses

  • 200 Stored state after the check, plus this check's result.
  • 401 Missing, malformed, revoked or paused API key.
  • 403 Missing scope, a restricted key, a paused project or another rule; code says which.
  • 404 No such resource in this project.
  • 429 The key's rate-limit bucket is used up (rate_limit_exceeded), or, on POST /v1/emails, the project's daily quota (daily_quota_exceeded).

Errors come in the error envelope; the codes are in Error codes.

200 response body

FieldTypeDescription
domainstring
statuspending | verified | failed | suspended
verifiedboolean
verificationobjectStored per-record flags from the last conclusive checks.
verification.spfboolean
verification.dkimboolean
verification.dmarcboolean
lastCheckedAtstring (date-time) | null
verifiedAtstring (date-time) | null
createdAtstring (date-time)
resultobject
result.spfpass | fail | unknownunknown = DNS didn't answer; never un-verifies a domain.
result.dkimpass | fail | unknownunknown = DNS didn't answer; never un-verifies a domain.
result.dmarcpass | fail | unknownunknown = DNS didn't answer; never un-verifies a domain.
result.overallpass | fail | unknownunknown = DNS didn't answer; never un-verifies a domain.
result.sourceauthoritative | public | recursiveWhich kind of source decided: the domain's own nameservers, the public resolvers postfly falls back to, or postfly's own resolver.
result.propagatingobject[]Records some sources see and others do not yet. They count as published; a domain becomes verified once a source other than its own nameservers sees them.
result.propagating[].recordspf | dkim | dmarc
result.propagating[].ttlinteger | nullSeconds the old value may still be served, from the nameservers' answer.
result.propagating[].seenByintegerSources that already see the record.
result.propagating[].ofintegerSources that answered at all.
result.propagating[].elsewherebooleanSomething other than the domain's own nameservers sees it.
result.sourcesobject[]What each source answered, and how to ask it yourself.
result.sources[].kindauthoritative | public | system
result.sources[].labelstringThe nameserver or resolver asked, as text.
result.sources[].spfpass | fail | unknownunknown = DNS didn't answer; never un-verifies a domain.
result.sources[].dkimpass | fail | unknownunknown = DNS didn't answer; never un-verifies a domain.
result.sources[].dmarcpass | fail | unknownunknown = DNS didn't answer; never un-verifies a domain.
result.sources[].digRecord<string, string>The equivalent dig command per record.

Suppressions

GET/v1/suppressions

The project's suppressions, newest first; a global row only for an exact `email`

Scope
suppressions:read
Rate limit
default

Query parameters

FieldTypeDescription
limitinteger1–100 · default 25
beforestringnextCursor of a previous page — continue towards older items. Not with after.
afterstringprevCursor of a previous page — continue towards newer items. Not with before.
scopeproject | globalglobal needs email (the global list is looked up, never browsed).
reasonhard_bounce | spam_complaint | unsubscribe | manual | blocklist
emailstring (email)Exact address, case-insensitive; also returns the global row for it.

Responses

  • 200 One page of suppressions.
  • 400 Validation failed (invalid_request, invalid_json, …).
  • 401 Missing, malformed, revoked or paused API key.
  • 403 Missing scope, a restricted key, a paused project or another rule; code says which.
  • 429 The key's rate-limit bucket is used up (rate_limit_exceeded), or, on POST /v1/emails, the project's daily quota (daily_quota_exceeded).

Errors come in the error envelope; the codes are in Error codes.

200 response body

FieldTypeDescription
dataobject[]
data[].emailstring
data[].scopeproject | global
data[].reasonhard_bounce | spam_complaint | unsubscribe | manual | blocklist
data[].detailstring | null
data[].messageIdstring | nullSource message; always null on global rows.
data[].createdAtstring (date-time)
hasMorebooleanMore items exist in the direction requested.
nextCursorstring | nullPass as before for older items.
prevCursorstring | nullPass as after for newer items.

POST/v1/suppressions

Add or update an address on the project's suppression list

Scope
suppressions:write
Rate limit
default

Request body

FieldTypeDescription
emailrequiredstring (email)max 254 characters
reasonobjectdefault manual
detailstringmax 200 characters
scopeproject | global"global" is refused (403 global_scope_forbidden).

Responses

  • 200 Updated an existing project suppression.
  • 201 Added.
  • 400 Validation failed (invalid_request, invalid_json, …).
  • 401 Missing, malformed, revoked or paused API key.
  • 403 Missing scope, a restricted key, a paused project or another rule; code says which.
  • 429 The key's rate-limit bucket is used up (rate_limit_exceeded), or, on POST /v1/emails, the project's daily quota (daily_quota_exceeded).

Errors come in the error envelope; the codes are in Error codes.

200 response body

FieldTypeDescription
emailstring
scopeproject | global
reasonhard_bounce | spam_complaint | unsubscribe | manual | blocklist
detailstring | null
messageIdstring | nullSource message; always null on global rows.
createdAtstring (date-time)

201 response body

FieldTypeDescription
emailstring
scopeproject | global
reasonhard_bounce | spam_complaint | unsubscribe | manual | blocklist
detailstring | null
messageIdstring | nullSource message; always null on global rows.
createdAtstring (date-time)

DELETE/v1/suppressions/{email}

Remove an address from the project's suppression list

Scope
suppressions:write
Rate limit
default

Path parameters

FieldTypeDescription
emailrequiredstringURL-encoded address, case-insensitive.

Responses

  • 200 Deleted from the project list.
  • 401 Missing, malformed, revoked or paused API key.
  • 403 Missing scope, a restricted key, a paused project or another rule; code says which.
  • 404 No such resource in this project.
  • 429 The key's rate-limit bucket is used up (rate_limit_exceeded), or, on POST /v1/emails, the project's daily quota (daily_quota_exceeded).

Errors come in the error envelope; the codes are in Error codes.

200 response body

FieldTypeDescription
emailstring
deletedtrue
stillSuppressedglobal | null"global" when the address remains on the global list.