Domains
Add the domain you send from, publish the DNS records PostStack generates for it, and it becomes verified — usually within a minute of the records going live.
How verification works#
Add the domain
POST /domainswith the domain name. PostStack generates a 2048-bit DKIM key for it and returns the DNS records to publish. The domain starts aspending.Publish the records
Create each record in
dnsRecordsat your DNS host. If you connected a DNS provider (HostStack or Cloudflare) in the dashboard, PostStack writes them for you.Wait for the check
Every pending domain is re-checked every 60 seconds, reading your authoritative nameservers first, so a record is usually seen seconds after you save it. Call
POST /domains/:id/verifyto check right away. The domain becomesverified— and adomain.verifiedwebhook fires — when every required record passes.
The required records are SPF and DKIM. DMARC, return-path, MX and the rest are recommended but never hold verification back — MX only governs receiving, so enabling inbound before you publish it leaves the domain sending, with the MX row pending until it resolves.
You can only send from a verified domain, and the from address must be on a domain you added exactly: sending as news.yourdomain.com needs that subdomain added as its own domain. Otherwise POST /emails answers 422 with Domain '…' is not verified or Domain '…' not found for this team.
Once verified, a domain is not re-verified every minute. A sweep checks SPF (and MX, if inbound is on) every 6 hours and fires domain.dns_drift when one stops matching, without stopping your sending. A domain only goes back to pending — which stops sending and fires domain.failed — when a check finds a required record missing outright; a record that is present but wrong (a second SPF record, a different DKIM key) is reported, not enforced.
DNS records#
These are the exact records PostStack generates, with yourdomain.com standing in for your domain. Always copy the values from dnsRecords — the DKIM key is unique to your domain.
| Purpose | Type | Name | Value | Needed for |
|---|---|---|---|---|
spf | TXT | yourdomain.com | v=spf1 include:poststack.dev ~all | Verification |
dkim | TXT | poststack._domainkey.yourdomain.com | v=DKIM1; k=rsa; h=sha256; p=<your key> | Verification |
dmarc | TXT | _dmarc.yourdomain.com | v=DMARC1; p=quarantine; sp=quarantine; adkim=r; aspf=r; pct=100; rua=mailto:dmarc@poststack.dev | Recommended |
return_path | CNAME | bounce.yourdomain.com | bounce.poststack.dev | Recommended |
mx | MX | yourdomain.com | inbound.poststack.dev, priority 10 | Receiving, once inbound is enabled (does not affect sending) |
What each check accepts
- SPF passes when your domain's SPF record contains
include:poststack.dev. A domain may publish only onev=spf1record: if you already have one, add the include to it rather than creating a second — with two, every receiver treats SPF as broken and the check fails. - DKIM passes only when the published key is exactly the one PostStack signs with. The value is longer than the 255 characters one DNS string can hold; hosts that ask you to split it are fine, the check reassembles it.
- DMARC passes with any
v=DMARC1record, so keep your own policy if you have one. Addingrua=mailto:dmarc@poststack.devis what lets PostStack collect the aggregate reports behind the DMARC endpoints below. - Return path must be a CNAME to
bounce.poststack.dev. Once it verifies, bounces are addressed tobounce.yourdomain.com(or<label>.yourdomain.comif you setcustom_return_path), which lets SPF align with your domain under DMARC; without it they use PostStack's own bounce domain and DMARC relies on DKIM alone.
Records that appear later
| Purpose | When | Record |
|---|---|---|
dkim_rotation | During a DKIM key rotation | TXT at <new selector>._domainkey.yourdomain.com. After the switch-over the same row describes the old key, which must stay published until it is retired. |
bimi | bimi_logo_url is set | TXT at default._bimi.yourdomain.com: v=BIMI1; l=<logo URL> |
imap, submission | The domain has an active mailbox | SRV at _imaps._tcp.yourdomain.com (0 1 993 poststack.dev) and _submissions._tcp.yourdomain.com (0 1 465 poststack.dev), so mail apps can find the servers. |
postmaster | After verification | A google-site-verification=… TXT at the apex, so PostStack can read Gmail Postmaster Tools data for the domain. |
None of these hold verification back.
Endpoints#
All endpoints live under https://api.poststack.dev and authenticate with Authorization: Bearer <key>. Reading needs a full-access API key or the domains:read scope; changing anything needs domains:manage. Errors come back as { "error": "…" }, and a body that fails validation is a 400 naming the field.
Add a domain#
/domainsAdds a domain to your team, generates its DKIM key and DNS records, and returns them. A domain can belong to only one PostStack team.
{
"name": "yourdomain.com"
}HTTP/1.1 201 Created
{
"domain": {
"id": 42,
"publicId": "dom_4k9x2m7q1w8r3t6y0p5s2d8a",
"teamId": 3051,
"name": "yourdomain.com",
"region": "eu-west-1",
"status": "pending",
"dkimPublicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu3…IDAQAB",
"dkimSelector": "poststack",
"dkimPendingPublicKey": null,
"dkimPendingSelector": null,
"dkimRotationStartedAt": null,
"dkimRotationPublishedAt": null,
"dkimRetiredSelector": null,
"dkimRetiredPublicKey": null,
"dkimRetireAfter": null,
"customReturnPath": "bounce",
"openTracking": true,
"clickTracking": true,
"trackingDomain": null,
"tlsMode": "opportunistic",
"inboundEnabled": false,
"catchAll": false,
"catchAllMailboxId": null,
"bimiLogoUrl": null,
"dnsProvider": null,
"ipAddressId": null,
"streamPreference": "any",
"manualReviewReason": null,
"verifiedAt": null,
"dnsIntegrationZoneId": null,
"dnsIntegrationRecordIds": null,
"dnsIntegrationSyncedAt": null,
"dnsIntegrationError": null,
"delegationCheckCode": null,
"delegationCheckHint": null,
"delegationNameservers": [],
"createdAt": "2026-09-29T10:00:00.000Z",
"updatedAt": "2026-09-29T10:04:12.000Z",
"dnsRecords": [
{
"id": 311,
"domainId": 42,
"recordType": "TXT",
"name": "yourdomain.com",
"value": "v=spf1 include:poststack.dev ~all",
"purpose": "spf",
"verified": false,
"lastCheckedAt": null,
"lastCheckCode": null,
"lastCheckHint": null,
"createdAt": "2026-09-29T10:00:00.000Z",
"updatedAt": "2026-09-29T10:04:12.000Z"
},
{
"id": 312,
"domainId": 42,
"recordType": "TXT",
"name": "poststack._domainkey.yourdomain.com",
"value": "v=DKIM1; k=rsa; h=sha256; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu3…IDAQAB",
"purpose": "dkim",
"verified": false,
…
},
{
"id": 313,
"domainId": 42,
"recordType": "TXT",
"name": "_dmarc.yourdomain.com",
"value": "v=DMARC1; p=quarantine; sp=quarantine; adkim=r; aspf=r; pct=100; rua=mailto:dmarc@poststack.dev",
"purpose": "dmarc",
"verified": false,
…
},
{
"id": 314,
"domainId": 42,
"recordType": "CNAME",
"name": "bounce.yourdomain.com",
"value": "bounce.poststack.dev",
"purpose": "return_path",
"verified": false,
…
}
]
}
}// The SDK unwraps { domain }.
const domain = await poststack.domains.create({ name: 'yourdomain.com' });
for (const r of domain.dnsRecords) {
console.log(r.recordType, r.name, r.value);
}curl -X POST https://api.poststack.dev/domains \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "name": "yourdomain.com" }'Body parameters
namestringrequired- The domain, e.g.
yourdomain.comormail.yourdomain.com. Up to 253 characters; surrounding spaces, a trailing dot and upper case are normalised away. No scheme, no path. region"eu-west-1"defaulteu-west-1- The only region. See Data regions.
open_trackingbooleandefaulttrue- Add the open-tracking pixel to HTML mail from this domain.
click_trackingbooleandefaulttrue- Rewrite links in HTML mail for click tracking.
tls_mode"opportunistic" | "enforced"defaultopportunistic- See Update a domain.
custom_return_pathstringdefaultbounce- The label of the return-path host,
<label>.yourdomain.com— a single DNS label (letters, digits and hyphens), not a hostname. The return-path CNAME is generated at that host, and once it verifies, mail is sent with that host as its bounce address and bounces sent back to it are processed like any other. Set it at creation; it cannot be changed afterwards.
Response fields
domain.idinteger- Numeric ID. Accepted anywhere the path takes :id.
domain.publicIdstringdom_followed by 24 characters.domain.teamIdinteger- The team that owns the domain.
domain.namestring- The domain, lower-cased, without a trailing dot.
domain.region"eu-west-1"- Where its mail is sent from. Always eu-west-1.
domain.status"pending" | "verified"- Only a
verifieddomain can send. The column also allowsfailed, but no current code path sets it. domain.verifiedAtstring (ISO 8601) | null- When it last became verified.
domain.manualReviewReasonstring | null- Set when verification is held for a manual review — e.g.
new_domain_<7d (age=2)for a domain registered less than 7 days ago. The domain stayspendinguntil PostStack clears it, even with correct DNS. domain.dkimSelectorstring- The selector signing now —
poststackuntil you rotate. domain.dkimPublicKeystring- The base64 public key published in the DKIM record. The private key is never returned.
domain.dkimPendingSelector, dkimPendingPublicKey, dkimRotationStartedAt, dkimRotationPublishedAtstring | null- The staged key of a DKIM rotation in progress; null otherwise.
domain.dkimRetiredSelector, dkimRetiredPublicKey, dkimRetireAfterstring | null- The previous key after a rotation, and when its record will be retired; null otherwise.
domain.customReturnPathstring- The return-path label;
bounceby default. domain.openTrackingboolean- Whether the open pixel is added to HTML mail by default.
domain.clickTrackingboolean- Whether links are rewritten for click tracking by default.
domain.trackingDomainstring | null- A custom tracking hostname. Currently always null — see Update a domain.
domain.tlsMode"opportunistic" | "enforced"- See Update a domain.
domain.inboundEnabledboolean- Whether PostStack accepts mail for this domain.
domain.catchAllboolean- Whether unmatched addresses are delivered to catchAllMailboxId.
domain.catchAllMailboxIdinteger | null- The catch-all destination mailbox.
domain.bimiLogoUrlstring | null- The BIMI logo URL.
domain.streamPreference"any" | "transactional" | "marketing" | "notification"- See Update a domain.
domain.ipAddressIdinteger | null- The dedicated IP it sends from, or null for the shared pool.
domain.dnsProviderstring | null- The DNS host PostStack detected for the domain, when known.
domain.dnsIntegrationZoneId, dnsIntegrationRecordIds, dnsIntegrationSyncedAt, dnsIntegrationErrorvarious | null- State of the automatic record push when a DNS provider (HostStack or Cloudflare) is connected in the dashboard; null otherwise.
domain.delegationCheckCode, delegationCheckHint, delegationNameserversstring | null, string | null, string[]- Set when none of the records resolve at all: why (
domain_not_delegated,records_added_at_the_wrong_provider,delegation_unknown), what to do, and which nameservers are authoritative today. domain.createdAt, updatedAtstring (ISO 8601)- Timestamps.
domain.dnsRecords[].recordType"TXT" | "CNAME" | "MX" | "SRV"- The record type to create.
domain.dnsRecords[].namestring- The full hostname — some DNS hosts want only the part before your domain.
domain.dnsRecords[].valuestring- The value to publish.
domain.dnsRecords[].purposestring- What the record is for — see DNS records.
domain.dnsRecords[].verifiedboolean- Whether the last check found it.
domain.dnsRecords[].lastCheckedAtstring (ISO 8601) | null- When it was last checked.
domain.dnsRecords[].lastCheckCode, lastCheckHintstring | null- Why the last check failed, and how to fix it — see Troubleshooting.
domain.dnsRecords[].id, domainId, createdAt, updatedAtinteger / string- Row metadata.
Status codes
| Status | Meaning |
|---|---|
| 201 | Created, with its DNS records. |
| 400 | The name is not a valid domain, or another field failed validation. |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:manage. Also returned when the team is suspended. Also Verify your email address before adding a domain. and Domain limit reached for <plan> plan (N domains) — Free 1, Starter 3, Pro 10, Scale 1,000, Enterprise unlimited. |
| 409 | Domain already registered — on this or any other PostStack team. |
| 422 | Domain rejected — please contact support if you believe this is in error: the name matched PostStack's abuse blocklist. |
| 429 | More than 10 calls a minute with the same credential. Wait for the number of seconds in Retry-After. |
List domains#
/domainsEvery domain on your team, newest first, each with its DNS records. Not paginated.
{
"domains": [
{
"id": 42,
"publicId": "dom_4k9x2m7q1w8r3t6y0p5s2d8a",
"name": "yourdomain.com",
"status": "verified",
…,
"dnsRecords": [ … ]
}
]
}const { domains } = await poststack.domains.list();curl https://api.poststack.dev/domains \
-H "Authorization: Bearer sk_live_..."Response fields
domains[]object[]- Each has every field of domain in Add a domain, dnsRecords included.
Status codes
| Status | Meaning |
|---|---|
| 200 | The list (possibly empty). |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended. |
Retrieve a domain#
/domains/:idOne domain with its DNS records and each record's latest check result.
{
"domain": {
"id": 42,
"publicId": "dom_4k9x2m7q1w8r3t6y0p5s2d8a",
"teamId": 3051,
"name": "yourdomain.com",
"region": "eu-west-1",
"status": "verified",
"dkimPublicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu3…IDAQAB",
"dkimSelector": "poststack",
"dkimPendingPublicKey": null,
"dkimPendingSelector": null,
"dkimRotationStartedAt": null,
"dkimRotationPublishedAt": null,
"dkimRetiredSelector": null,
"dkimRetiredPublicKey": null,
"dkimRetireAfter": null,
"customReturnPath": "bounce",
"openTracking": true,
"clickTracking": true,
"trackingDomain": null,
"tlsMode": "opportunistic",
"inboundEnabled": false,
"catchAll": false,
"catchAllMailboxId": null,
"bimiLogoUrl": null,
"dnsProvider": null,
"ipAddressId": null,
"streamPreference": "any",
"manualReviewReason": null,
"verifiedAt": "2026-09-29T10:04:12.000Z",
"dnsIntegrationZoneId": null,
"dnsIntegrationRecordIds": null,
"dnsIntegrationSyncedAt": null,
"dnsIntegrationError": null,
"delegationCheckCode": null,
"delegationCheckHint": null,
"delegationNameservers": [],
"createdAt": "2026-09-29T10:00:00.000Z",
"updatedAt": "2026-09-29T10:04:12.000Z",
"dnsRecords": [
{
"id": 311,
"domainId": 42,
"recordType": "TXT",
"name": "yourdomain.com",
"value": "v=spf1 include:poststack.dev ~all",
"purpose": "spf",
"verified": true,
"lastCheckedAt": "2026-09-29T10:04:12.000Z",
"lastCheckCode": null,
"lastCheckHint": null,
"createdAt": "2026-09-29T10:00:00.000Z",
"updatedAt": "2026-09-29T10:04:12.000Z"
},
{
"id": 312,
"domainId": 42,
"recordType": "TXT",
"name": "poststack._domainkey.yourdomain.com",
"value": "v=DKIM1; k=rsa; h=sha256; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu3…IDAQAB",
"purpose": "dkim",
"verified": true,
…
},
{
"id": 313,
"domainId": 42,
"recordType": "TXT",
"name": "_dmarc.yourdomain.com",
"value": "v=DMARC1; p=quarantine; sp=quarantine; adkim=r; aspf=r; pct=100; rua=mailto:dmarc@poststack.dev",
"purpose": "dmarc",
"verified": true,
…
},
{
"id": 314,
"domainId": 42,
"recordType": "CNAME",
"name": "bounce.yourdomain.com",
"value": "bounce.poststack.dev",
"purpose": "return_path",
"verified": true,
…
}
]
}
}const domain = await poststack.domains.get(42);curl https://api.poststack.dev/domains/dom_4k9x2m7q1w8r3t6y0p5s2d8a \
-H "Authorization: Bearer sk_live_..."Path parameters
idstringrequired- The domain's
dom_…ID or its numericid— both are accepted.
Response fields
domain.idinteger- Numeric ID. Accepted anywhere the path takes :id.
domain.publicIdstringdom_followed by 24 characters.domain.teamIdinteger- The team that owns the domain.
domain.namestring- The domain, lower-cased, without a trailing dot.
domain.region"eu-west-1"- Where its mail is sent from. Always eu-west-1.
domain.status"pending" | "verified"- Only a
verifieddomain can send. The column also allowsfailed, but no current code path sets it. domain.verifiedAtstring (ISO 8601) | null- When it last became verified.
domain.manualReviewReasonstring | null- Set when verification is held for a manual review — e.g.
new_domain_<7d (age=2)for a domain registered less than 7 days ago. The domain stayspendinguntil PostStack clears it, even with correct DNS. domain.dkimSelectorstring- The selector signing now —
poststackuntil you rotate. domain.dkimPublicKeystring- The base64 public key published in the DKIM record. The private key is never returned.
domain.dkimPendingSelector, dkimPendingPublicKey, dkimRotationStartedAt, dkimRotationPublishedAtstring | null- The staged key of a DKIM rotation in progress; null otherwise.
domain.dkimRetiredSelector, dkimRetiredPublicKey, dkimRetireAfterstring | null- The previous key after a rotation, and when its record will be retired; null otherwise.
domain.customReturnPathstring- The return-path label;
bounceby default. domain.openTrackingboolean- Whether the open pixel is added to HTML mail by default.
domain.clickTrackingboolean- Whether links are rewritten for click tracking by default.
domain.trackingDomainstring | null- A custom tracking hostname. Currently always null — see Update a domain.
domain.tlsMode"opportunistic" | "enforced"- See Update a domain.
domain.inboundEnabledboolean- Whether PostStack accepts mail for this domain.
domain.catchAllboolean- Whether unmatched addresses are delivered to catchAllMailboxId.
domain.catchAllMailboxIdinteger | null- The catch-all destination mailbox.
domain.bimiLogoUrlstring | null- The BIMI logo URL.
domain.streamPreference"any" | "transactional" | "marketing" | "notification"- See Update a domain.
domain.ipAddressIdinteger | null- The dedicated IP it sends from, or null for the shared pool.
domain.dnsProviderstring | null- The DNS host PostStack detected for the domain, when known.
domain.dnsIntegrationZoneId, dnsIntegrationRecordIds, dnsIntegrationSyncedAt, dnsIntegrationErrorvarious | null- State of the automatic record push when a DNS provider (HostStack or Cloudflare) is connected in the dashboard; null otherwise.
domain.delegationCheckCode, delegationCheckHint, delegationNameserversstring | null, string | null, string[]- Set when none of the records resolve at all: why (
domain_not_delegated,records_added_at_the_wrong_provider,delegation_unknown), what to do, and which nameservers are authoritative today. domain.createdAt, updatedAtstring (ISO 8601)- Timestamps.
domain.dnsRecords[].recordType"TXT" | "CNAME" | "MX" | "SRV"- The record type to create.
domain.dnsRecords[].namestring- The full hostname — some DNS hosts want only the part before your domain.
domain.dnsRecords[].valuestring- The value to publish.
domain.dnsRecords[].purposestring- What the record is for — see DNS records.
domain.dnsRecords[].verifiedboolean- Whether the last check found it.
domain.dnsRecords[].lastCheckedAtstring (ISO 8601) | null- When it was last checked.
domain.dnsRecords[].lastCheckCode, lastCheckHintstring | null- Why the last check failed, and how to fix it — see Troubleshooting.
domain.dnsRecords[].id, domainId, createdAt, updatedAtinteger / string- Row metadata.
Status codes
| Status | Meaning |
|---|---|
| 200 | The domain. |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended. |
| 404 | Domain not found — no domain with this ID belongs to your team. |
Update a domain#
/domains/:idChanges a domain's sending and receiving settings. Send only the fields you want to change. The response is the domain without dnsRecords — retrieve it to see records this change added or removed.
{
"tls_mode": "enforced",
"stream_preference": "transactional"
}{
"domain": {
"id": 42,
"publicId": "dom_4k9x2m7q1w8r3t6y0p5s2d8a",
"teamId": 3051,
"name": "yourdomain.com",
"region": "eu-west-1",
"status": "verified",
"dkimPublicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu3…IDAQAB",
"dkimSelector": "poststack",
"dkimPendingPublicKey": null,
"dkimPendingSelector": null,
"dkimRotationStartedAt": null,
"dkimRotationPublishedAt": null,
"dkimRetiredSelector": null,
"dkimRetiredPublicKey": null,
"dkimRetireAfter": null,
"customReturnPath": "bounce",
"openTracking": true,
"clickTracking": true,
"trackingDomain": null,
"tlsMode": "enforced",
"inboundEnabled": false,
"catchAll": false,
"catchAllMailboxId": null,
"bimiLogoUrl": null,
"dnsProvider": null,
"ipAddressId": null,
"streamPreference": "transactional",
"manualReviewReason": null,
"verifiedAt": "2026-09-29T10:04:12.000Z",
"dnsIntegrationZoneId": null,
"dnsIntegrationRecordIds": null,
"dnsIntegrationSyncedAt": null,
"dnsIntegrationError": null,
"delegationCheckCode": null,
"delegationCheckHint": null,
"delegationNameservers": [],
"createdAt": "2026-09-29T10:00:00.000Z",
"updatedAt": "2026-09-29T10:04:12.000Z"
}
}// Returns the updated domain (the SDK unwraps { domain }).
const domain = await poststack.domains.update(42, {
tls_mode: 'enforced',
stream_preference: 'transactional',
});curl -X PATCH https://api.poststack.dev/domains/dom_4k9x2m7q1w8r3t6y0p5s2d8a \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "tls_mode": "enforced" }'Path parameters
idstringrequired- The domain's
dom_…ID or its numericid— both are accepted.
Body parameters
open_trackingboolean- Default for the open-tracking pixel. A send can override it.
click_trackingboolean- Default for click tracking. A send can override it.
tls_mode"opportunistic" | "enforced"opportunisticuses TLS whenever the receiving server offers it and plaintext otherwise.enforcednever delivers in plaintext: mail to a server that does not offer STARTTLS is deferred and retried, and bounces if it never does.inbound_enabledboolean- Accept mail for this domain. Turning it on adds the
mxrecord and makes it required; turning it off removes it. See the warning below. catch_allboolean- Deliver mail for addresses that match no mailbox to
catch_all_mailbox_id. Enabling it needs a destination (sent now or already stored); disabling it clears the destination. catch_all_mailbox_idinteger | null- An active mailbox on this domain. null clears it.
stream_preference"any" | "transactional" | "marketing" | "notification"- The kind of mail this domain sends. It sets queue priority —
transactionalfirst, thennotificationandany, thenmarketing— and must match the stream of an assigned dedicated IP unless either side isany. bimi_logo_urlstring | null- An
https://URL to your BIMI logo (SVG Tiny PS), up to 2,048 characters. Adds thebimirecord; null removes it. Mailbox providers only show a BIMI logo for domains whose DMARC policy is enforced (quarantineorreject). tracking_domainstring | null- A custom hostname for tracking links. Currently unavailable: any value other than null or an empty string returns
503, and tracking links use PostStack's own domain. Clearing it always works.
Response fields
domain.idinteger- Numeric ID. Accepted anywhere the path takes :id.
domain.publicIdstringdom_followed by 24 characters.domain.teamIdinteger- The team that owns the domain.
domain.namestring- The domain, lower-cased, without a trailing dot.
domain.region"eu-west-1"- Where its mail is sent from. Always eu-west-1.
domain.status"pending" | "verified"- Only a
verifieddomain can send. The column also allowsfailed, but no current code path sets it. domain.verifiedAtstring (ISO 8601) | null- When it last became verified.
domain.manualReviewReasonstring | null- Set when verification is held for a manual review — e.g.
new_domain_<7d (age=2)for a domain registered less than 7 days ago. The domain stayspendinguntil PostStack clears it, even with correct DNS. domain.dkimSelectorstring- The selector signing now —
poststackuntil you rotate. domain.dkimPublicKeystring- The base64 public key published in the DKIM record. The private key is never returned.
domain.dkimPendingSelector, dkimPendingPublicKey, dkimRotationStartedAt, dkimRotationPublishedAtstring | null- The staged key of a DKIM rotation in progress; null otherwise.
domain.dkimRetiredSelector, dkimRetiredPublicKey, dkimRetireAfterstring | null- The previous key after a rotation, and when its record will be retired; null otherwise.
domain.customReturnPathstring- The return-path label;
bounceby default. domain.openTrackingboolean- Whether the open pixel is added to HTML mail by default.
domain.clickTrackingboolean- Whether links are rewritten for click tracking by default.
domain.trackingDomainstring | null- A custom tracking hostname. Currently always null — see Update a domain.
domain.tlsMode"opportunistic" | "enforced"- See Update a domain.
domain.inboundEnabledboolean- Whether PostStack accepts mail for this domain.
domain.catchAllboolean- Whether unmatched addresses are delivered to catchAllMailboxId.
domain.catchAllMailboxIdinteger | null- The catch-all destination mailbox.
domain.bimiLogoUrlstring | null- The BIMI logo URL.
domain.streamPreference"any" | "transactional" | "marketing" | "notification"- See Update a domain.
domain.ipAddressIdinteger | null- The dedicated IP it sends from, or null for the shared pool.
domain.dnsProviderstring | null- The DNS host PostStack detected for the domain, when known.
domain.dnsIntegrationZoneId, dnsIntegrationRecordIds, dnsIntegrationSyncedAt, dnsIntegrationErrorvarious | null- State of the automatic record push when a DNS provider (HostStack or Cloudflare) is connected in the dashboard; null otherwise.
domain.delegationCheckCode, delegationCheckHint, delegationNameserversstring | null, string | null, string[]- Set when none of the records resolve at all: why (
domain_not_delegated,records_added_at_the_wrong_provider,delegation_unknown), what to do, and which nameservers are authoritative today. domain.createdAt, updatedAtstring (ISO 8601)- Timestamps.
Status codes
| Status | Meaning |
|---|---|
| 200 | The updated domain. An empty body changes nothing and returns it as is. |
| 400 | Validation failed, or Catch-all requires a destination mailbox on this domain / Catch-all mailbox must be an active mailbox on this domain. |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:manage. Also returned when the team is suspended. |
| 404 | Domain not found — no domain with this ID belongs to your team. |
| 409 | The new stream_preference does not match the stream of the dedicated IP assigned to the domain. |
| 503 | tracking_domain was set — custom tracking domains are unavailable. |
Delete a domain#
/domains/:idDeletes the domain and everything that depends on it: its DNS records, its mailboxes and aliases, and its DKIM key. API keys restricted to this domain are expired, not widened to the rest of your team. Emails already sent keep their history. If a DNS provider is connected, the records PostStack pushed there are removed too. This cannot be undone.
{
"success": true
}await poststack.domains.delete(42);curl -X DELETE https://api.poststack.dev/domains/dom_4k9x2m7q1w8r3t6y0p5s2d8a \
-H "Authorization: Bearer sk_live_..."Path parameters
idstringrequired- The domain's
dom_…ID or its numericid— both are accepted.
Response fields
successboolean- Always true.
Status codes
| Status | Meaning |
|---|---|
| 200 | Deleted. |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:manage. Also returned when the team is suspended. |
| 404 | Domain not found — no domain with this ID belongs to your team. |
Check DNS now#
/domains/:id/verifyQueues an immediate DNS check and returns the domain as it is at that moment — the check runs in the background, so status and dnsRecords[].verified change a few seconds later. Retrieve the domain to see the result. Works on verified domains too, where a missing required record moves the domain back to pending.
{
"queued": true,
"domain": {
"id": 42,
"publicId": "dom_4k9x2m7q1w8r3t6y0p5s2d8a",
"teamId": 3051,
"name": "yourdomain.com",
"region": "eu-west-1",
"status": "pending",
"dkimPublicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu3…IDAQAB",
"dkimSelector": "poststack",
"dkimPendingPublicKey": null,
"dkimPendingSelector": null,
"dkimRotationStartedAt": null,
"dkimRotationPublishedAt": null,
"dkimRetiredSelector": null,
"dkimRetiredPublicKey": null,
"dkimRetireAfter": null,
"customReturnPath": "bounce",
"openTracking": true,
"clickTracking": true,
"trackingDomain": null,
"tlsMode": "opportunistic",
"inboundEnabled": false,
"catchAll": false,
"catchAllMailboxId": null,
"bimiLogoUrl": null,
"dnsProvider": null,
"ipAddressId": null,
"streamPreference": "any",
"manualReviewReason": null,
"verifiedAt": null,
"dnsIntegrationZoneId": null,
"dnsIntegrationRecordIds": null,
"dnsIntegrationSyncedAt": null,
"dnsIntegrationError": null,
"delegationCheckCode": null,
"delegationCheckHint": null,
"delegationNameservers": [],
"createdAt": "2026-09-29T10:00:00.000Z",
"updatedAt": "2026-09-29T10:04:12.000Z",
"dnsRecords": [ … ]
}
}// The SDK returns the domain snapshot from before the check.
await poststack.domains.verify(42);
// A few seconds later:
const { status, dnsRecords } = await poststack.domains.get(42);curl -X POST https://api.poststack.dev/domains/dom_4k9x2m7q1w8r3t6y0p5s2d8a/verify \
-H "Authorization: Bearer sk_live_..."Path parameters
idstringrequired- The domain's
dom_…ID or its numericid— both are accepted.
Response fields
queuedboolean- Always true.
domainobject- The domain with its DNS records, before the check.
Status codes
| Status | Meaning |
|---|---|
| 200 | The check was queued. |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:manage. Also returned when the team is suspended. |
| 404 | Domain not found — no domain with this ID belongs to your team. |
| 429 | More than 10 calls in 15 minutes with the same credential. Wait for the number of seconds in Retry-After. |
Check a from address#
/domains/check-deliverabilityOne-call pre-flight for a from address: whether its domain is on your team and verified, which authentication records pass, its bounce and complaint rates over the last 30 days, and warnings. For an address whose domain you have not added, it still answers 200 — with domain_verified false and an unknown_domain warning.
{
"from": "billing@yourdomain.com",
"domain": "yourdomain.com",
"domain_id": 42,
"domain_verified": true,
"domain_status": "verified",
"dkim": { "configured": true, "verified": true },
"spf": { "configured": true, "verified": true },
"dmarc": { "configured": true, "verified": true },
"return_path": { "configured": true, "verified": false },
"volume_30d": {
"attempted": 12480,
"bounced": 96,
"complained": 2,
"events_recorded": 12480,
"reserved_excluded": 14
},
"bounce_rate": 0.0077,
"complaint_rate": 0.00016,
"warnings": []
}curl "https://api.poststack.dev/domains/check-deliverability?from=billing%40yourdomain.com" \
-H "Authorization: Bearer sk_live_..."Query parameters
fromstringrequired- A from address, bare or
Name <address>(URL-encoded).
Response fields
fromstring- The address you passed, trimmed.
domainstring- Its domain.
domain_idinteger- The domain’s numeric ID. Absent for an unknown domain.
domain_verifiedboolean- Whether the domain can send.
domain_statusstring- The domain’s status. Absent for an unknown domain.
spf, dkim, dmarc, return_path{ configured, verified }- Whether PostStack generated that record for the domain, and whether it passed its last check. return_path is absent for an unknown domain.
volume_30dobjectattempted,bounced,complained,events_recordedandreserved_excluded(test and probe recipients left out of the counts). Absent for an unknown domain.bounce_rate, complaint_ratenumber- Fractions of attempted, 0–1.
warnings[]object[]{ severity: "warning" | "info", code, message }. Codes includeunknown_domain,missing_dmarc,missing_text_body,html_too_large,noreply_sender,link_domain_mismatch,high_bounce_rate(above 2%) andhigh_complaint_rate(above 0.3%).
Status codes
| Status | Meaning |
|---|---|
| 200 | The check. |
| 400 | from query parameter required, or no domain could be parsed from it. |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended. |
DKIM key rotation#
Rotate a domain's DKIM key if it may have been exposed, or as routine hygiene. The new key is published under a new selector, so both keys resolve at once and there is no moment when mail is signed with a key receivers cannot look up. Your sending is never interrupted.
Start
PostStack generates a new key under a selector like
ps-20260929-a1b2and returns one TXT record to publish. Your current key keeps signing. A rotation you start and never finish changes nothing.Switch over
Once the new record is published and matches, PostStack starts signing with it — automatically, 30 minutes after it first sees the record (checked every 15 minutes), because receivers' resolvers lag behind your nameservers. Call activate to switch sooner.
Retire
The old key's record must stay published for 7 days after the switch-over. Then PostStack retires it, and removes it from your DNS provider if one is connected.
The three phases fire domain.dkim_rotation_started, domain.dkim_rotation_activated and domain.dkim_rotation_completed webhooks. All three endpoints return the rotation's state:
Start a rotation#
/domains/:id/dkim/rotateGenerates a new key under a new selector and returns the TXT record to publish.
HTTP/1.1 201 Created
{
"rotation": {
"domainId": 42,
"state": "pending_dns",
"activeSelector": "poststack",
"pendingSelector": "ps-20260929-a1b2",
"pendingPublicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx7…IDAQAB",
"pendingRecordName": "ps-20260929-a1b2._domainkey.yourdomain.com",
"pendingRecordValue": "v=DKIM1; k=rsa; h=sha256; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx7…IDAQAB",
"startedAt": "2026-09-29T09:00:00.000Z",
"publishedAt": null,
"activatesAfter": null,
"retiredSelector": null,
"retireAfter": null
}
}// The SDK unwraps { rotation }.
const rotation = await poststack.domains.startDkimRotation(42);
// Publish this TXT record, then wait — the switch-over is automatic.
console.log(rotation.pendingRecordName, rotation.pendingRecordValue);curl -X POST https://api.poststack.dev/domains/dom_4k9x2m7q1w8r3t6y0p5s2d8a/dkim/rotate \
-H "Authorization: Bearer sk_live_..."Path parameters
idstringrequired- The domain's
dom_…ID or its numericid— both are accepted.
Response fields
rotation.domainIdinteger- The domain’s numeric ID.
rotation.state"idle" | "pending_dns" | "ready" | "retiring"pending_dns: waiting for the new record.ready: record seen, waiting out the 30 minutes.retiring: switched over, old record still needed.idle: no rotation.rotation.activeSelectorstring- The selector signing now.
rotation.pendingSelectorstring | null- The new selector, until switch-over.
rotation.pendingPublicKeystring | null- Its public key.
rotation.pendingRecordNamestring | null- Where to publish the new TXT record.
rotation.pendingRecordValuestring | null- Its value.
rotation.startedAtstring (ISO 8601) | null- When the rotation started.
rotation.publishedAtstring (ISO 8601) | null- When PostStack first saw the new record.
rotation.activatesAfterstring (ISO 8601) | null- The earliest automatic switch-over: publishedAt + 30 minutes.
rotation.retiredSelectorstring | null- The previous selector, while it is retiring.
rotation.retireAfterstring (ISO 8601) | null- When the old record will be retired.
Status codes
| Status | Meaning |
|---|---|
| 201 | Started. |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:manage. Also returned when the team is suspended. |
| 404 | Domain not found — no domain with this ID belongs to your team. |
| 409 | A rotation is already in progress, the previous one is still retiring its old key, or the domain has no DKIM key. |
| 429 | More than 5 calls an hour with the same credential. Wait for the number of seconds in Retry-After. |
Switch to the new key#
/domains/:id/dkim/rotate/activateChecks that the new record is published with the right key and switches signing to it. Without force it also waits until 30 minutes after the record was first seen, and answers 409 with the time to try again. If nothing could be confirmed, nothing changes.
{
"rotation": {
"domainId": 42,
"state": "retiring",
"activeSelector": "ps-20260929-a1b2",
"pendingSelector": null,
"pendingPublicKey": null,
"pendingRecordName": null,
"pendingRecordValue": null,
"startedAt": null,
"publishedAt": null,
"activatesAfter": null,
"retiredSelector": "poststack",
"retireAfter": "2026-10-06T09:40:00.000Z"
}
}// Wait for the propagation window (the safe default):
await poststack.domains.activateDkimRotation(42);
// Or skip the wait once the record resolves publicly:
await poststack.domains.activateDkimRotation(42, { force: true });curl -X POST "https://api.poststack.dev/domains/dom_4k9x2m7q1w8r3t6y0p5s2d8a/dkim/rotate/activate?force=true" \
-H "Authorization: Bearer sk_live_..."Path parameters
idstringrequired- The domain's
dom_…ID or its numericid— both are accepted.
Query parameters
force"true" | "false"defaultfalsetrueskips the 30-minute wait — use it only once you have confirmed the record resolves publicly (dig TXT <pendingRecordName>). It never skips the DNS check.
Response fields
rotationobject- As in Start a rotation, now with
state: "retiring".
Status codes
| Status | Meaning |
|---|---|
| 200 | Switched over. |
| 400 | force is not true or false. |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:manage. Also returned when the team is suspended. |
| 404 | Domain not found — no domain with this ID belongs to your team. |
| 409 | No rotation in progress; the new record is not published or carries a different key; or it is published but the 30 minutes have not passed (the message says until when). |
| 429 | More than 20 calls in 15 minutes with the same credential. Wait for the number of seconds in Retry-After. |
| 503 | DNS could not be read right now. Nothing was changed; try again shortly. |
Cancel a rotation#
/domains/:id/dkim/rotateAbandons a rotation that has not switched over yet, discards the new key and removes its record from the domain's list (and from your DNS provider, if connected). The staged key never signed anything, so this is always safe. After the switch-over there is nothing to cancel.
{
"rotation": {
"domainId": 42,
"state": "idle",
"activeSelector": "poststack",
"pendingSelector": null,
"pendingPublicKey": null,
"pendingRecordName": null,
"pendingRecordValue": null,
"startedAt": null,
"publishedAt": null,
"activatesAfter": null,
"retiredSelector": null,
"retireAfter": null
}
}await poststack.domains.cancelDkimRotation(42);curl -X DELETE https://api.poststack.dev/domains/dom_4k9x2m7q1w8r3t6y0p5s2d8a/dkim/rotate \
-H "Authorization: Bearer sk_live_..."Path parameters
idstringrequired- The domain's
dom_…ID or its numericid— both are accepted.
Response fields
rotationobject- As in Start a rotation, back to
state: "idle".
Status codes
| Status | Meaning |
|---|---|
| 200 | Cancelled. |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:manage. Also returned when the team is suspended. |
| 404 | Domain not found — no domain with this ID belongs to your team. |
| 409 | No DKIM rotation is in progress for this domain. |
| 429 | More than 10 calls an hour with the same credential. Wait for the number of seconds in Retry-After. |
Dedicated IPs#
By default a domain sends from PostStack's shared pool. A dedicated IP is a paid add-on (€25 a month) bought in the dashboard; assigning one to a domain moves all of that domain's mail onto it.
Assign a dedicated IP#
/domains/:id/ipSends this domain's mail from one of your team's dedicated IPs. Replaces any IP already assigned.
{
"ipAddressId": 7
}{
"success": true
}await poststack.domains.assignIp(42, 7);curl -X POST https://api.poststack.dev/domains/dom_4k9x2m7q1w8r3t6y0p5s2d8a/ip \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "ipAddressId": 7 }'Path parameters
idstringrequired- The domain's
dom_…ID or its numericid— both are accepted.
Body parameters
ipAddressIdintegerrequired- The ID of a dedicated IP assigned to your team.
Response fields
successboolean- Always true.
Status codes
| Status | Meaning |
|---|---|
| 200 | Assigned. |
| 400 | ipAddressId is missing or not a positive integer. |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:manage. Also returned when the team is suspended. |
| 404 | Domain not found, or IP address not found or not assigned to your team. |
| 409 | Stream mismatch: the domain's stream_preference and the IP's stream differ and neither is any. |
| 429 | More than 10 calls a minute with the same credential. Wait for the number of seconds in Retry-After. |
Remove the dedicated IP#
/domains/:id/ipMoves the domain back to the shared pool. Succeeds even if no IP was assigned.
{
"success": true
}await poststack.domains.unassignIp(42);curl -X DELETE https://api.poststack.dev/domains/dom_4k9x2m7q1w8r3t6y0p5s2d8a/ip \
-H "Authorization: Bearer sk_live_..."Path parameters
idstringrequired- The domain's
dom_…ID or its numericid— both are accepted.
Response fields
successboolean- Always true.
Status codes
| Status | Meaning |
|---|---|
| 200 | Unassigned. |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:manage. Also returned when the team is suspended. |
| 404 | Domain not found — no domain with this ID belongs to your team. |
| 429 | More than 10 calls a minute with the same credential. Wait for the number of seconds in Retry-After. |
DMARC reports#
Mailbox providers send daily aggregate DMARC reports to the address in your DMARC record's rua= tag. With rua=mailto:dmarc@poststack.dev in it, PostStack ingests them and these endpoints summarise them: who is sending as your domain, and whether that mail passes. The SPF and DKIM counts are the receivers' alignment-aware results, not raw checks. Windows count reports by when PostStack received them.
List DMARC reports#
/domains/:id/dmarc/reportsThe aggregate reports received for the domain, newest first.
{
"reports": [
{
"id": 1882,
"teamId": 3051,
"domainId": 42,
"reportId": "13071991585245581023",
"orgName": "google.com",
"email": "noreply-dmarc-support@google.com",
"dateRangeStart": "2026-09-28T00:00:00.000Z",
"dateRangeEnd": "2026-09-28T23:59:59.000Z",
"domain": "yourdomain.com",
"policyPublished": { "adkim": "r", "aspf": "r", "p": "quarantine", "sp": "quarantine", "pct": 100 },
"status": "processed",
"createdAt": "2026-09-29T06:12:40.000Z"
}
],
"pagination": { "page": 1, "perPage": 20, "total": 15, "totalPages": 1 }
}curl "https://api.poststack.dev/domains/42/dmarc/reports?page=1&perPage=50" \
-H "Authorization: Bearer sk_live_..."Path parameters
idstringrequired- The domain's
dom_…ID or its numericid— both are accepted.
Query parameters
pageintegerdefault1- Page number, from 1.
perPageintegerdefault20- Reports per page; values above 100 are capped at 100. Note the camelCase name.
Response fields
reports[].idinteger- PostStack’s ID for the report.
reports[].reportIdstring- The reporter’s own report ID.
reports[].orgNamestring- Who sent it, e.g. google.com.
reports[].emailstring | null- The reporter’s contact address.
reports[].domainstring- The domain the report covers.
reports[].dateRangeStart, dateRangeEndstring (ISO 8601) | null- The period the report covers.
reports[].policyPublishedobject | null- Your DMARC policy as the reporter saw it.
reports[].status"pending" | "processed" | "error"- Ingestion state; reports you can read here are
processed. reports[].teamId, domainId, createdAtinteger / string- Ownership and receipt time.
paginationobjectpage,perPage,total,totalPages.
Status codes
| Status | Meaning |
|---|---|
| 200 | The page of reports. |
| 400 | page or perPage is not a positive integer. |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended. |
| 404 | Domain not found — no domain with this ID belongs to your team. |
Get DMARC stats#
/domains/:id/dmarc/statsTotals over the window plus one row per day that had reports. Cached for 5 minutes.
{
"totals": { "totalMessages": 45000, "passed": 44865, "spfPassed": 44790, "dkimPassed": 44860 },
"daily": [
{ "date": "2026-09-28", "total": 1503, "passed": 1500, "failed": 3 },
{ "date": "2026-09-29", "total": 1481, "passed": 1480, "failed": 1 }
]
}const stats = await poststack.domains.getDmarcStats(42, 30);curl "https://api.poststack.dev/domains/42/dmarc/stats?days=30" \
-H "Authorization: Bearer sk_live_..."Path parameters
idstringrequired- The domain's
dom_…ID or its numericid— both are accepted.
Query parameters
daysintegerdefault30- How many days back to include, 1–90.
Response fields
totals.totalMessagesinteger- Messages covered by the reports.
totals.passedinteger- Messages that passed DMARC.
totals.spfPassed, dkimPassedinteger- Messages whose SPF / DKIM result was pass.
daily[]object[]date(YYYY-MM-DD),total,passed,failed. Days without reports are omitted.
Status codes
| Status | Meaning |
|---|---|
| 200 | The stats. |
| 400 | days is not a whole number from 1 to 90. |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended. |
| 404 | Domain not found — no domain with this ID belongs to your team. |
List DMARC sources#
/domains/:id/dmarc/sourcesEvery IP address that sent mail as your domain, busiest first. An unfamiliar IP with failures is either a service you forgot to authorise or someone spoofing you.
{
"sources": [
{ "sourceIp": "198.51.100.1", "total": 12500, "passed": 12500, "failed": 0 },
{ "sourceIp": "203.0.113.50", "total": 5, "passed": 0, "failed": 5 }
]
}const { sources } = await poststack.domains.getDmarcSources(42, 30);curl "https://api.poststack.dev/domains/42/dmarc/sources?days=30" \
-H "Authorization: Bearer sk_live_..."Path parameters
idstringrequired- The domain's
dom_…ID or its numericid— both are accepted.
Query parameters
daysintegerdefault30- How many days back to include, 1–90.
Response fields
sources[].sourceIpstring- The sending IP.
sources[].total, passed, failedinteger- Messages from it, and how many passed or failed DMARC.
Status codes
| Status | Meaning |
|---|---|
| 200 | The sources. |
| 400 | days is not a whole number from 1 to 90. |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended. |
| 404 | Domain not found — no domain with this ID belongs to your team. |
DMARC results by provider#
/domains/:id/dmarc/providersThe same results grouped by the organisation that reported them, busiest first — so you can see that mail passes at Gmail but fails at Yahoo.
{
"providers": [
{ "provider": "google.com", "total": 38200, "passed": 38150, "spfPassed": 38090, "dkimPassed": 38148 },
{ "provider": "Yahoo", "total": 4100, "passed": 4012, "spfPassed": 4003, "dkimPassed": 4011 }
]
}curl "https://api.poststack.dev/domains/42/dmarc/providers?days=30" \
-H "Authorization: Bearer sk_live_..."Path parameters
idstringrequired- The domain's
dom_…ID or its numericid— both are accepted.
Query parameters
daysintegerdefault30- How many days back to include, 1–90.
Response fields
providers[].providerstring- The reporting organisation, e.g. google.com.
providers[].total, passed, spfPassed, dkimPassedinteger- Messages, DMARC passes, and SPF / DKIM passes.
Status codes
| Status | Meaning |
|---|---|
| 200 | The breakdown. |
| 400 | days is not a whole number from 1 to 90. |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended. |
| 404 | Domain not found — no domain with this ID belongs to your team. |
Deliverability#
What happened to the domain's mail after it left PostStack, and how PostStack rates the domain. None of these can tell you whether a message landed in the inbox or the spam folder — no mailbox provider reports that — so they report the evidence that predicts it.
Get delivery outcomes#
/domains/:id/placementPer-provider outcomes for the domain's mail and every throttle or block response grouped by cause. Accepted means the receiving server returned 250. Test-mode sends are left out.
{
"windowDays": 30,
"totals": {
"sent": 12500,
"accepted": 12410,
"deferred": 62,
"bounced": 28,
"complained": 4,
"acceptanceRate": 0.9928,
"complaintRate": 0.00032,
"avgSpamScore": 1.65
},
"senderSignals": [
{
"bucket": "throttled",
"ownership": "sender",
"provider": "gmail",
"code": "4.7.28",
"message": "421-4.7.28 Gmail has detected an unusual rate of mail originating from your domain",
"messages": 47,
"lastSeenAt": "2026-09-29T09:14:02.000Z"
}
],
"recipientSignals": [],
"providers": [
{ "provider": "gmail", "sent": 9800, "accepted": 9740, "deferred": 44, "bounced": 16, "complained": 3, "acceptanceRate": 0.9939 }
]
}const placement = await poststack.domains.getInboxPlacement(42, 30);curl "https://api.poststack.dev/domains/42/placement?days=30" \
-H "Authorization: Bearer sk_live_..."Path parameters
idstringrequired- The domain's
dom_…ID or its numericid— both are accepted.
Query parameters
daysintegerdefault30- The window in days, 1–90.
Response fields
windowDaysinteger- The window actually used.
totals.sent, accepted, deferred, bounced, complainedinteger- Message counts by final outcome.
totals.acceptanceRatenumber | null- accepted ÷ sent, 0–1; null when nothing was sent.
totals.complaintRatenumber | null- complained ÷ accepted, 0–1.
totals.avgSpamScorenumber | null- PostStack’s average pre-send spam score for the domain’s mail.
senderSignals[]object[]- Up to 8 rejection patterns you can act on, worst first:
bucket(throttled,reputation_block,authentication,content,policy,unknown),ownership,provider,code,message,messages,lastSeenAt. recipientSignals[]object[]- Up to 8 recipient-side patterns (
recipient,infrastructure) — bad addresses and unreachable servers, not a problem with your sending. providers[]object[]provider,sent,accepted,deferred,bounced,complained,acceptanceRate.
Status codes
| Status | Meaning |
|---|---|
| 200 | The summary. |
| 400 | days is not a whole number from 1 to 90. |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended. |
| 404 | Domain not found — no domain with this ID belongs to your team. |
Get mailbox-provider signals#
/domains/:id/deliverability-signalsThe latest value of every signal PostStack collects from mailbox providers about the domain — Gmail Postmaster Tools, Microsoft SNDS and feedback-loop complaints — from the last 45 days.
{
"domainId": 42,
"signals": [
{
"provider": "gmail",
"source": "postmaster",
"metric": "spam_rate",
"valueNum": 0.0004,
"valueText": null,
"observedAt": "2026-09-28T00:00:00.000Z"
}
],
"gmailSpamRate": 0.0004,
"outlookHealth": null,
"fblComplaints30d": 1
}curl https://api.poststack.dev/domains/42/deliverability-signals \
-H "Authorization: Bearer sk_live_..."Path parameters
idstringrequired- The domain's
dom_…ID or its numericid— both are accepted.
Response fields
domainIdinteger- The domain’s numeric ID.
signals[]object[]- The newest value per
provider+source+metric, withvalueNum,valueTextandobservedAt. gmailSpamRatenumber | null- Gmail Postmaster Tools’ latest user-reported spam rate; null without Postmaster data.
outlookHealthstring | null- Microsoft SNDS’s latest verdict; null without SNDS data.
fblComplaints30dinteger- Feedback-loop complaints in the last 30 days.
Status codes
| Status | Meaning |
|---|---|
| 200 | The signals (possibly empty). |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended. |
| 404 | Domain not found — no domain with this ID belongs to your team. |
Get the reputation score#
/domains/:id/reputationPostStack's latest 0–100 reputation score for the domain and the factors behind it. Bounces, complaints, blocklist status and SPF/DKIM/DMARC make up the score; open and click rates are shown but not scored. A domain that has sent nothing scores 100.
{
"reputation": {
"score": 94,
"factors": {
"totalSent": 12500,
"bounceRate": 0.0022,
"complaintRate": 0.0003,
"openRate": 0.41,
"clickRate": 0.06,
"engagementRate": 0.43,
"blocklistClean": true,
"spfVerified": true,
"dkimVerified": true,
"dmarcVerified": true
},
"calculatedAt": "2026-09-29T06:00:00.000Z"
}
}curl https://api.poststack.dev/domains/42/reputation \
-H "Authorization: Bearer sk_live_..."Path parameters
idstringrequired- The domain's
dom_…ID or its numericid— both are accepted.
Response fields
reputationobject | null- null until a score has been calculated.
reputation.scoreinteger- 0–100.
reputation.factorsobjecttotalSent,bounceRate,complaintRate,openRate,clickRate,engagementRate(fractions, 0–1),blocklistClean,spfVerified,dkimVerified,dmarcVerified.reputation.calculatedAtstring (ISO 8601)- When it was calculated.
Status codes
| Status | Meaning |
|---|---|
| 200 | The score. |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended. |
| 404 | Domain not found — no domain with this ID belongs to your team. |
Get reputation history#
/domains/:id/reputation/historyThe last score of each UTC day, oldest first. Days with no calculation are omitted.
{
"history": [
{ "date": "2026-09-27", "score": 91 },
{ "date": "2026-09-28", "score": 93 },
{ "date": "2026-09-29", "score": 94 }
]
}curl "https://api.poststack.dev/domains/42/reputation/history?days=30" \
-H "Authorization: Bearer sk_live_..."Path parameters
idstringrequired- The domain's
dom_…ID or its numericid— both are accepted.
Query parameters
daysintegerdefault30- How many days back, 1–365.
Response fields
history[]object[]date(YYYY-MM-DD) andscore.
Status codes
| Status | Meaning |
|---|---|
| 200 | The history. |
| 400 | days is not an integer from 1 to 365. |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended. |
| 404 | Domain not found — no domain with this ID belongs to your team. |
Get daily volume#
/domains/:id/timeseriesDaily sent, delivered, bounced, opened and clicked counts for the domain's mail.
{
"timeseries": [
{ "date": "2026-09-28", "sent": 1480, "delivered": 1471, "bounced": 4, "opened": 602, "clicked": 88 },
{ "date": "2026-09-29", "sent": 910, "delivered": 905, "bounced": 1, "opened": 310, "clicked": 41 }
]
}curl "https://api.poststack.dev/domains/42/timeseries?days=7" \
-H "Authorization: Bearer sk_live_..."Path parameters
idstringrequired- The domain's
dom_…ID or its numericid— both are accepted.
Query parameters
daysintegerdefault307,30or90. Any other value from 1 to 365 is treated as 30.
Response fields
timeseries[]object[]date,sent,delivered,bounced,opened,clicked.
Status codes
| Status | Meaning |
|---|---|
| 200 | The series. |
| 400 | days is not an integer from 1 to 365. |
| 401 | Missing, invalid or revoked API key or access token. |
| 403 | insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended. |
| 404 | Domain not found — no domain with this ID belongs to your team. |
Troubleshooting#
When a record fails, its lastCheckCode and lastCheckHint say why. The hint is written for you and says what to change.
| lastCheckCode | What it means |
|---|---|
record_not_found | Nothing is published at that name yet — or your DNS host has not saved it. |
spf_merge_needed | You already have an SPF record. Add include:poststack.dev to it instead of creating a second one; the hint shows the merged value. |
multiple_spf_records | Two v=spf1 records are published. Merge them into one. |
dkim_key_mismatch | A DKIM record exists at the name but holds a different key — often left by a previous provider, or only half of the key was pasted. |
missing_trailing_dot | A CNAME or MX target was entered without a trailing dot, so your host appended your domain to it. |
points_elsewhere | The CNAME or MX points somewhere other than the expected target. |
null_mx | An "MX 0 ." record (“this domain accepts no mail”) is published next to ours. Delete it. |
mx_outranked | Our MX is published, but another MX has a lower priority number, so incoming mail goes there instead. |
return_path_drift | The return-path CNAME verified once and has since stopped resolving to bounce.poststack.dev. |
Every record says record_not_found, but I added them
Check delegationCheckCode on the domain. records_added_at_the_wrong_provider means your domain's nameservers are at a different host from the one where you added the records — delegationNameservers lists the ones that count. Add the records there. domain_not_delegated means the domain has no nameservers at all.
My DNS host shows the record, but it has not verified
Many hosts append your domain to the name you type, so entering poststack._domainkey.yourdomain.com creates poststack._domainkey.yourdomain.com.yourdomain.com. Enter only the part before your domain (poststack._domainkey, _dmarc, bounce), or @ for the domain itself. Then call POST /domains/:id/verify.
All records verified, but the domain is still pending
Look at manualReviewReason. A domain registered less than 7 days ago waits for a manual review; nothing on your side will change that sooner.
Sending stopped and the domain is pending again
A required record went missing — most often the MX after enabling inbound, or an SPF or DKIM record removed while changing email providers. Retrieve the domain, republish whatever is not verified, and call POST /domains/:id/verify.