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).
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
Query parameters
| Field | Type | Description |
|---|---|---|
| limit | integer | 1–100 · default 25 |
| before | string | nextCursor of a previous page — continue towards older items. Not with after. |
| after | string | prevCursor of a previous page — continue towards newer items. Not with before. |
| status | queued | sending | sent | delivered | bounced | complained | failed | |
| to | string (email) | Recipient address, case-insensitive exact match. |
| from | string (email) | Sender address, case-insensitive exact match. |
| tag | string | max 64 characters |
| since | string (date-time) | Created at or after (inclusive). |
| until | string (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;
codesays 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
| Field | Type | Description |
|---|---|---|
| data | object[] | |
| data[].id | string | |
| data[].status | queued | sending | sent | delivered | bounced | complained | failed | |
| data[].from | string | |
| data[].to | string | |
| data[].subject | string | |
| data[].tag | string | null | |
| data[].bounce | object | null | Null unless the message bounced. |
| data[].bounce.reason | string | The bounce class: transient, mailbox_not_found, domain_not_found, policy_block, invalid_recipient or unknown. |
| data[].bounce.category | hard_bounce | soft_bounce | null | |
| data[].smtpResponse | string | null | |
| data[].timestamps | object | |
| data[].timestamps.created | string (date-time) | |
| data[].timestamps.sent | string (date-time) | null | |
| data[].timestamps.delivered | string (date-time) | null | |
| data[].timestamps.failed | string (date-time) | null | |
| hasMore | boolean | More items exist in the direction requested. |
| nextCursor | string | null | Pass as before for older items. |
| prevCursor | string | null | Pass as after for newer items. |
POST/v1/emails
Queue a transactional email
Request body
Either html or text is required.
| Field | Type | Description |
|---|---|---|
| fromrequired | string (email) | Address on a verified sender domain of the project.max 254 characters |
| fromName | string | No line breaks.max 255 characters |
| torequired | string (email) | Exactly one recipient.max 254 characters |
| replyTo | string (email) | max 254 characters |
| subjectrequired | string | No line breaks.min 1 characters · max 998 characters |
| html | string | max 26214400 characters |
| text | string | max 26214400 characters |
| headers | Record<string, string> | Extra headers; ones postfly composes itself are ignored. |
| tag | string | max 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;
codesays 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
| Field | Type | Description |
|---|---|---|
| id | string | |
| status | "queued" | |
| createdAt | string (date-time) |
GET/v1/emails/{id}
One message and its delivery status
Path parameters
| Field | Type | Description |
|---|---|---|
| idrequired | string | pattern ^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;
codesays 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
| Field | Type | Description |
|---|---|---|
| id | string | |
| status | queued | sending | sent | delivered | bounced | complained | failed | |
| from | string | |
| to | string | |
| subject | string | |
| tag | string | null | |
| bounce | object | null | Null unless the message bounced. |
| bounce.reason | string | The bounce class: transient, mailbox_not_found, domain_not_found, policy_block, invalid_recipient or unknown. |
| bounce.category | hard_bounce | soft_bounce | null | |
| smtpResponse | string | null | |
| timestamps | object | |
| timestamps.created | string (date-time) | |
| timestamps.sent | string (date-time) | null | |
| timestamps.delivered | string (date-time) | null | |
| timestamps.failed | string (date-time) | null |
Sender domains
GET/v1/domains
The project's sender domains (at most 50, not paginated)
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;
codesays 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
| Field | Type | Description |
|---|---|---|
| data | object[] | |
| data[].domain | string | |
| data[].status | pending | verified | failed | suspended | |
| data[].verified | boolean | |
| data[].verification | object | Stored per-record flags from the last conclusive checks. |
| data[].verification.spf | boolean | |
| data[].verification.dkim | boolean | |
| data[].verification.dmarc | boolean | |
| data[].lastCheckedAt | string (date-time) | null | |
| data[].verifiedAt | string (date-time) | null | |
| data[].createdAt | string (date-time) |
POST/v1/domains
Register a sender domain and get the DNS records to publish
Request body
domain, or Resend-style name.
| Field | Type | Description |
|---|---|---|
| domain | string | max 253 characters |
| name | string | max 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;
codesays 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
| Field | Type | Description |
|---|---|---|
| domain | string | |
| status | pending | verified | failed | suspended | |
| verified | boolean | |
| verification | object | Stored per-record flags from the last conclusive checks. |
| verification.spf | boolean | |
| verification.dkim | boolean | |
| verification.dmarc | boolean | |
| lastCheckedAt | string (date-time) | null | |
| verifiedAt | string (date-time) | null | |
| createdAt | string (date-time) | |
| records | object[] | |
| records[].purpose | spf | dkim | dmarc | mx | |
| records[].type | TXT | MX | |
| records[].name | string | Fully qualified record name. |
| records[].value | string | null | null when postfly can't tell what to publish — see warning. |
| records[].required | boolean | false for the optional MX record. |
| records[].verified | boolean | null | null for MX (not checked). |
| records[].published | boolean | The 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[].replaces | string | null | SPF only: the current record to overwrite (one SPF record per domain). |
| records[].lookups | integer | null | SPF only: DNS-lookup terms in value (limit 10). |
| records[].warning | current_record_unavailable | too_many_dns_lookups | null | SPF only. |
GET/v1/domains/{domain}
Domain status and the DNS records to publish
Path parameters
| Field | Type | Description |
|---|---|---|
| domainrequired | string |
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;
codesays 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
| Field | Type | Description |
|---|---|---|
| domain | string | |
| status | pending | verified | failed | suspended | |
| verified | boolean | |
| verification | object | Stored per-record flags from the last conclusive checks. |
| verification.spf | boolean | |
| verification.dkim | boolean | |
| verification.dmarc | boolean | |
| lastCheckedAt | string (date-time) | null | |
| verifiedAt | string (date-time) | null | |
| createdAt | string (date-time) | |
| records | object[] | |
| records[].purpose | spf | dkim | dmarc | mx | |
| records[].type | TXT | MX | |
| records[].name | string | Fully qualified record name. |
| records[].value | string | null | null when postfly can't tell what to publish — see warning. |
| records[].required | boolean | false for the optional MX record. |
| records[].verified | boolean | null | null for MX (not checked). |
| records[].published | boolean | The 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[].replaces | string | null | SPF only: the current record to overwrite (one SPF record per domain). |
| records[].lookups | integer | null | SPF only: DNS-lookup terms in value (limit 10). |
| records[].warning | current_record_unavailable | too_many_dns_lookups | null | SPF only. |
DELETE/v1/domains/{domain}
Remove a sender domain from the project
Path parameters
| Field | Type | Description |
|---|---|---|
| domainrequired | string |
Responses
- 200 Deleted.
- 401 Missing, malformed, revoked or paused API key.
- 403 Missing scope, a restricted key, a paused project or another rule;
codesays 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
| Field | Type | Description |
|---|---|---|
| domain | string | |
| deleted | true |
POST/v1/domains/{domain}/verify
Re-run the SPF / DKIM / DMARC checks now
Path parameters
| Field | Type | Description |
|---|---|---|
| domainrequired | string |
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;
codesays 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
| Field | Type | Description |
|---|---|---|
| domain | string | |
| status | pending | verified | failed | suspended | |
| verified | boolean | |
| verification | object | Stored per-record flags from the last conclusive checks. |
| verification.spf | boolean | |
| verification.dkim | boolean | |
| verification.dmarc | boolean | |
| lastCheckedAt | string (date-time) | null | |
| verifiedAt | string (date-time) | null | |
| createdAt | string (date-time) | |
| result | object | |
| result.spf | pass | fail | unknown | unknown = DNS didn't answer; never un-verifies a domain. |
| result.dkim | pass | fail | unknown | unknown = DNS didn't answer; never un-verifies a domain. |
| result.dmarc | pass | fail | unknown | unknown = DNS didn't answer; never un-verifies a domain. |
| result.overall | pass | fail | unknown | unknown = DNS didn't answer; never un-verifies a domain. |
| result.source | authoritative | public | recursive | Which kind of source decided: the domain's own nameservers, the public resolvers postfly falls back to, or postfly's own resolver. |
| result.propagating | object[] | 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[].record | spf | dkim | dmarc | |
| result.propagating[].ttl | integer | null | Seconds the old value may still be served, from the nameservers' answer. |
| result.propagating[].seenBy | integer | Sources that already see the record. |
| result.propagating[].of | integer | Sources that answered at all. |
| result.propagating[].elsewhere | boolean | Something other than the domain's own nameservers sees it. |
| result.sources | object[] | What each source answered, and how to ask it yourself. |
| result.sources[].kind | authoritative | public | system | |
| result.sources[].label | string | The nameserver or resolver asked, as text. |
| result.sources[].spf | pass | fail | unknown | unknown = DNS didn't answer; never un-verifies a domain. |
| result.sources[].dkim | pass | fail | unknown | unknown = DNS didn't answer; never un-verifies a domain. |
| result.sources[].dmarc | pass | fail | unknown | unknown = DNS didn't answer; never un-verifies a domain. |
| result.sources[].dig | Record<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`
Query parameters
| Field | Type | Description |
|---|---|---|
| limit | integer | 1–100 · default 25 |
| before | string | nextCursor of a previous page — continue towards older items. Not with after. |
| after | string | prevCursor of a previous page — continue towards newer items. Not with before. |
| scope | project | global | global needs email (the global list is looked up, never browsed). |
| reason | hard_bounce | spam_complaint | unsubscribe | manual | blocklist | |
| string (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;
codesays 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
| Field | Type | Description |
|---|---|---|
| data | object[] | |
| data[].email | string | |
| data[].scope | project | global | |
| data[].reason | hard_bounce | spam_complaint | unsubscribe | manual | blocklist | |
| data[].detail | string | null | |
| data[].messageId | string | null | Source message; always null on global rows. |
| data[].createdAt | string (date-time) | |
| hasMore | boolean | More items exist in the direction requested. |
| nextCursor | string | null | Pass as before for older items. |
| prevCursor | string | null | Pass as after for newer items. |
POST/v1/suppressions
Add or update an address on the project's suppression list
Request body
| Field | Type | Description |
|---|---|---|
| emailrequired | string (email) | max 254 characters |
| reason | object | default manual |
| detail | string | max 200 characters |
| scope | project | 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;
codesays 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
| Field | Type | Description |
|---|---|---|
| string | ||
| scope | project | global | |
| reason | hard_bounce | spam_complaint | unsubscribe | manual | blocklist | |
| detail | string | null | |
| messageId | string | null | Source message; always null on global rows. |
| createdAt | string (date-time) |
201 response body
| Field | Type | Description |
|---|---|---|
| string | ||
| scope | project | global | |
| reason | hard_bounce | spam_complaint | unsubscribe | manual | blocklist | |
| detail | string | null | |
| messageId | string | null | Source message; always null on global rows. |
| createdAt | string (date-time) |
DELETE/v1/suppressions/{email}
Remove an address from the project's suppression list
Path parameters
| Field | Type | Description |
|---|---|---|
| emailrequired | string | URL-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;
codesays 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
| Field | Type | Description |
|---|---|---|
| string | ||
| deleted | true | |
| stillSuppressed | global | null | "global" when the address remains on the global list. |