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 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 and encrypted: pass one back unchanged rather than parsing or building one, and treat anything we cannot read — edited, truncated, or issued before a key rotation — as 400 invalid_request and a reason to start the list again.

Emails

GET/v1/emails

List the messages this key can see, 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, unknown_field, invalid_json, …).
  • 401 Missing, malformed, revoked or paused API key.
  • 403 Missing scope, a restricted key, paused sending 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 daily sending quota (daily_quota_exceeded) or too many large sends at once (send_concurrency_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 | nullNull after content retention, 30 days after the message reaches its final state.
data[].tagstring | null
data[].attachmentsobject[]What was attached — never the content, which is deleted once the message is sent.
data[].attachments[].filenamestring
data[].attachments[].contentTypestring
data[].attachments[].sizeintegerBytes, decoded.
data[].attachments[].sha256stringHex SHA-256 of the file as sent.
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 this key may send from.max 254 characters
fromNamestringNo line breaks.max 255 characters
torequiredstring (email)Exactly one recipient.max 254 characters
replyTostring (email)max 254 characters
replyToNamestringDisplay name for replyTo. No line breaks.max 255 characters
subjectrequiredstringNo line breaks.min 1 characters · max 998 characters
htmlstringmax 10485760 characters
textstringmax 10485760 characters
headersRecord<string, string>Extra headers; ones postfly composes itself are ignored.
tagstringmax 64 characters · pattern ^[A-Za-z0-9._-]+$
attachmentsobject[]At most 10 files. Each at most 5 MiB decoded; the whole message as sent (headers, bodies, attachments base64-encoded, signature) at most 10 MiB.
attachments[].filenamerequiredstringPath segments and control characters are removed and the name is shortened to 150 characters. Executable and other dangerous extensions (exe, js, bat, scr, html, svg, …) are refused, and the extension must match contentType (pdf, xml, isdoc, isdocx, txt, csv, png, jpg, jpeg, zip).max 1024 characters
attachments[].contentTyperequiredstringOne of application/pdf, application/xml, text/xml, text/plain, text/csv, image/png, image/jpeg, application/zip, application/x-isdoc, application/x-isdocx. image/jpg, image/pjpeg and application/x-zip-compressed are read as their standard type; a text type may carry a charset parameter.max 255 characters
attachments[].contentrequiredstringStandard base64 of the file.

Responses

  • 202 Queued; delivery is asynchronous.
  • 400 Validation failed (invalid_request, unknown_field, invalid_json, …).
  • 401 Missing, malformed, revoked or paused API key.
  • 403 Missing scope, a restricted key, paused sending or another rule; code says which.
  • 409 Conflicts with existing state (domain_taken, recipient_suppressed).
  • 413 Over a size limit (payload_too_large, attachment_too_large, message_too_large).
  • 422 An attachment postfly does not send (attachment_type_not_allowed, attachment_extension_blocked, attachment_extension_mismatch, too_many_attachments).
  • 429 The key's rate-limit bucket is used up (rate_limit_exceeded), or, on POST /v1/emails, the daily sending quota (daily_quota_exceeded) or too many large sends at once (send_concurrency_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, unknown_field, invalid_json, …).
  • 401 Missing, malformed, revoked or paused API key.
  • 403 Missing scope, a restricted key, paused sending or another rule; code says which.
  • 404 No such resource this key can see.
  • 429 The key's rate-limit bucket is used up (rate_limit_exceeded), or, on POST /v1/emails, the daily sending quota (daily_quota_exceeded) or too many large sends at once (send_concurrency_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 | nullNull after content retention, 30 days after the message reaches its final state.
tagstring | null
attachmentsobject[]What was attached — never the content, which is deleted once the message is sent.
attachments[].filenamestring
attachments[].contentTypestring
attachments[].sizeintegerBytes, decoded.
attachments[].sha256stringHex SHA-256 of the file as sent.
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 sender domains this key can see (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, paused sending 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 daily sending quota (daily_quota_exceeded) or too many large sends at once (send_concurrency_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, unknown_field, invalid_json, …).
  • 401 Missing, malformed, revoked or paused API key.
  • 403 Missing scope, a restricted key, paused sending 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 daily sending quota (daily_quota_exceeded) or too many large sends at once (send_concurrency_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, paused sending or another rule; code says which.
  • 404 No such resource this key can see.
  • 429 The key's rate-limit bucket is used up (rate_limit_exceeded), or, on POST /v1/emails, the daily sending quota (daily_quota_exceeded) or too many large sends at once (send_concurrency_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 this key's group

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, paused sending or another rule; code says which.
  • 404 No such resource this key can see.
  • 429 The key's rate-limit bucket is used up (rate_limit_exceeded), or, on POST /v1/emails, the daily sending quota (daily_quota_exceeded) or too many large sends at once (send_concurrency_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, paused sending or another rule; code says which.
  • 404 No such resource this key can see.
  • 429 The key's rate-limit bucket is used up (rate_limit_exceeded), or, on POST /v1/emails, the daily sending quota (daily_quota_exceeded) or too many large sends at once (send_concurrency_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

Your 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 | account | globalproject (or its alias account) means your account's list, the same for every key; global 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, unknown_field, invalid_json, …).
  • 401 Missing, malformed, revoked or paused API key.
  • 403 Missing scope, a restricted key, paused sending 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 daily sending quota (daily_quota_exceeded) or too many large sends at once (send_concurrency_exceeded).

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

200 response body

FieldTypeDescription
dataobject[]
data[].emailstring
data[].scopeproject | globalproject is your account's list.
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 your suppression list

Scope
suppressions:write
Rate limit
default

Request body

FieldTypeDescription
emailrequiredstring (email)max 254 characters
reasonobjectdefault manual
detailstringmax 200 characters
scopeproject | account | globalaccount is the same as project: your account's list. "global" is refused (403 global_scope_forbidden).

Responses

  • 200 Updated an existing suppression of yours.
  • 201 Added.
  • 400 Validation failed (invalid_request, unknown_field, invalid_json, …).
  • 401 Missing, malformed, revoked or paused API key.
  • 403 Missing scope, a restricted key, paused sending 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 daily sending quota (daily_quota_exceeded) or too many large sends at once (send_concurrency_exceeded).

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

200 response body

FieldTypeDescription
emailstring
scopeproject | globalproject is your account's list.
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 | globalproject is your account's list.
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 your suppression list

Scope
suppressions:write
Rate limit
default

Path parameters

FieldTypeDescription
emailrequiredstringURL-encoded address, case-insensitive.

Responses

  • 200 Deleted from your list.
  • 401 Missing, malformed, revoked or paused API key.
  • 403 Missing scope, a restricted key, paused sending or another rule; code says which.
  • 404 No such resource this key can see.
  • 429 The key's rate-limit bucket is used up (rate_limit_exceeded), or, on POST /v1/emails, the daily sending quota (daily_quota_exceeded) or too many large sends at once (send_concurrency_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.