Skip to content

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#

  1. Add the domain

    POST /domains with the domain name. PostStack generates a 2048-bit DKIM key for it and returns the DNS records to publish. The domain starts as pending.

  2. Publish the records

    Create each record in dnsRecords at your DNS host. If you connected a DNS provider (HostStack or Cloudflare) in the dashboard, PostStack writes them for you.

  3. 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/verify to check right away. The domain becomes verified — and a domain.verified webhook 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.

New domains are reviewed first

A domain registered less than 7 days ago is held at pending for a manual review even when its DNS is correct; manualReviewReason says so. Freshly registered domains are the most common phishing vehicle.

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.

PurposeTypeNameValueNeeded for
spfTXTyourdomain.comv=spf1 include:poststack.dev ~allVerification
dkimTXTpoststack._domainkey.yourdomain.comv=DKIM1; k=rsa; h=sha256; p=<your key>Verification
dmarcTXT_dmarc.yourdomain.comv=DMARC1; p=quarantine; sp=quarantine; adkim=r; aspf=r; pct=100; rua=mailto:dmarc@poststack.devRecommended
return_pathCNAMEbounce.yourdomain.combounce.poststack.devRecommended
mxMXyourdomain.cominbound.poststack.dev, priority 10Receiving, 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 one v=spf1 record: 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=DMARC1 record, so keep your own policy if you have one. Adding rua=mailto:dmarc@poststack.dev is 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 to bounce.yourdomain.com (or <label>.yourdomain.com if you set custom_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

PurposeWhenRecord
dkim_rotationDuring a DKIM key rotationTXT 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.
bimibimi_logo_url is setTXT at default._bimi.yourdomain.com: v=BIMI1; l=<logo URL>
imap, submissionThe domain has an active mailboxSRV 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.
postmasterAfter verificationA 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#

POST
/domains

Adds a domain to your team, generates its DKIM key and DNS records, and returns them. A domain can belong to only one PostStack team.

json
{
  "name": "yourdomain.com"
}
json
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,
        …
      }
    ]
  }
}
typescript
// 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);
}
bash
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.com or mail.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"default eu-west-1
The only region. See Data regions.
open_trackingbooleandefault true
Add the open-tracking pixel to HTML mail from this domain.
click_trackingbooleandefault true
Rewrite links in HTML mail for click tracking.
tls_mode"opportunistic" | "enforced"default opportunistic
See Update a domain.
custom_return_pathstringdefault bounce
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.publicIdstring
dom_ 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 verified domain can send. The column also allows failed, 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 stays pending until PostStack clears it, even with correct DNS.
domain.dkimSelectorstring
The selector signing now — poststack until 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; bounce by 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

StatusMeaning
201Created, with its DNS records.
400The name is not a valid domain, or another field failed validation.
401Missing, invalid or revoked API key or access token.
403insufficient_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.
409Domain already registered — on this or any other PostStack team.
422Domain rejected — please contact support if you believe this is in error: the name matched PostStack's abuse blocklist.
429More than 10 calls a minute with the same credential. Wait for the number of seconds in Retry-After.

List domains#

GET
/domains

Every domain on your team, newest first, each with its DNS records. Not paginated.

json
{
  "domains": [
    {
      "id": 42,
      "publicId": "dom_4k9x2m7q1w8r3t6y0p5s2d8a",
      "name": "yourdomain.com",
      "status": "verified",
      …,
      "dnsRecords": [ … ]
    }
  ]
}
typescript
const { domains } = await poststack.domains.list();
bash
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

StatusMeaning
200The list (possibly empty).
401Missing, invalid or revoked API key or access token.
403insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended.

Retrieve a domain#

GET
/domains/:id

One domain with its DNS records and each record's latest check result.

json
{
  "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,
        …
      }
    ]
  }
}
typescript
const domain = await poststack.domains.get(42);
bash
curl https://api.poststack.dev/domains/dom_4k9x2m7q1w8r3t6y0p5s2d8a \
  -H "Authorization: Bearer sk_live_..."

Path parameters

idstringrequired
The domain's dom_… ID or its numeric id — both are accepted.

Response fields

domain.idinteger
Numeric ID. Accepted anywhere the path takes :id.
domain.publicIdstring
dom_ 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 verified domain can send. The column also allows failed, 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 stays pending until PostStack clears it, even with correct DNS.
domain.dkimSelectorstring
The selector signing now — poststack until 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; bounce by 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

StatusMeaning
200The domain.
401Missing, invalid or revoked API key or access token.
403insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended.
404Domain not found — no domain with this ID belongs to your team.

Update a domain#

PATCH
/domains/:id

Changes 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.

json
{
  "tls_mode": "enforced",
  "stream_preference": "transactional"
}
json
{
  "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"
  }
}
typescript
// Returns the updated domain (the SDK unwraps { domain }).
const domain = await poststack.domains.update(42, {
  tls_mode: 'enforced',
  stream_preference: 'transactional',
});
bash
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 numeric id — 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"
opportunistic uses TLS whenever the receiving server offers it and plaintext otherwise. enforced never 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 mx record 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 — transactional first, then notification and any, then marketing — and must match the stream of an assigned dedicated IP unless either side is any.
bimi_logo_urlstring | null
An https:// URL to your BIMI logo (SVG Tiny PS), up to 2,048 characters. Adds the bimi record; null removes it. Mailbox providers only show a BIMI logo for domains whose DMARC policy is enforced (quarantine or reject).
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.publicIdstring
dom_ 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 verified domain can send. The column also allows failed, 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 stays pending until PostStack clears it, even with correct DNS.
domain.dkimSelectorstring
The selector signing now — poststack until 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; bounce by 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

StatusMeaning
200The updated domain. An empty body changes nothing and returns it as is.
400Validation failed, or Catch-all requires a destination mailbox on this domain / Catch-all mailbox must be an active mailbox on this domain.
401Missing, invalid or revoked API key or access token.
403insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:manage. Also returned when the team is suspended.
404Domain not found — no domain with this ID belongs to your team.
409The new stream_preference does not match the stream of the dedicated IP assigned to the domain.
503tracking_domain was set — custom tracking domains are unavailable.

Publish the MX record before you enable inbound on a verified domain

Enabling inbound_enabled makes the MX record required and re-checks the domain straight away. If inbound.poststack.dev is not already your domain's MX, the check moves the domain back to pending, which stops all sending from it until the MX resolves. Publish the MX first — at priority 10, and with no other MX at a lower number, or your mail keeps going to the other server — then enable inbound. See Inbound email.

Delete a domain#

DELETE
/domains/:id

Deletes 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.

json
{
  "success": true
}
typescript
await poststack.domains.delete(42);
bash
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 numeric id — both are accepted.

Response fields

successboolean
Always true.

Status codes

StatusMeaning
200Deleted.
401Missing, invalid or revoked API key or access token.
403insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:manage. Also returned when the team is suspended.
404Domain not found — no domain with this ID belongs to your team.

Check DNS now#

POST
/domains/:id/verify

Queues 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.

json
{
  "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": [ … ]
  }
}
typescript
// 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);
bash
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 numeric id — both are accepted.

Response fields

queuedboolean
Always true.
domainobject
The domain with its DNS records, before the check.

Status codes

StatusMeaning
200The check was queued.
401Missing, invalid or revoked API key or access token.
403insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:manage. Also returned when the team is suspended.
404Domain not found — no domain with this ID belongs to your team.
429More than 10 calls in 15 minutes with the same credential. Wait for the number of seconds in Retry-After.

Check a from address#

GET
/domains/check-deliverability

One-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.

json
{
  "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": []
}
bash
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_30dobject
attempted, bounced, complained, events_recorded and reserved_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 include unknown_domain, missing_dmarc, missing_text_body, html_too_large, noreply_sender, link_domain_mismatch, high_bounce_rate (above 2%) and high_complaint_rate (above 0.3%).

Status codes

StatusMeaning
200The check.
400from query parameter required, or no domain could be parsed from it.
401Missing, invalid or revoked API key or access token.
403insufficient_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.

  1. Start

    PostStack generates a new key under a selector like ps-20260929-a1b2 and returns one TXT record to publish. Your current key keeps signing. A rotation you start and never finish changes nothing.

  2. 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.

  3. 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.

Do not delete the old record early

Mail sent before the switch-over is signed with the old key, and a receiving server still retrying one of those messages looks the key up at delivery time. Removing the record early turns those messages into DKIM failures.

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#

POST
/domains/:id/dkim/rotate

Generates a new key under a new selector and returns the TXT record to publish.

json
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
  }
}
typescript
// 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);
bash
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 numeric id — 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

StatusMeaning
201Started.
401Missing, invalid or revoked API key or access token.
403insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:manage. Also returned when the team is suspended.
404Domain not found — no domain with this ID belongs to your team.
409A rotation is already in progress, the previous one is still retiring its old key, or the domain has no DKIM key.
429More than 5 calls an hour with the same credential. Wait for the number of seconds in Retry-After.

Switch to the new key#

POST
/domains/:id/dkim/rotate/activate

Checks 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.

json
{
  "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"
  }
}
typescript
// 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 });
bash
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 numeric id — both are accepted.

Query parameters

force"true" | "false"default false
true skips 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

StatusMeaning
200Switched over.
400force is not true or false.
401Missing, invalid or revoked API key or access token.
403insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:manage. Also returned when the team is suspended.
404Domain not found — no domain with this ID belongs to your team.
409No 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).
429More than 20 calls in 15 minutes with the same credential. Wait for the number of seconds in Retry-After.
503DNS could not be read right now. Nothing was changed; try again shortly.

Cancel a rotation#

DELETE
/domains/:id/dkim/rotate

Abandons 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.

json
{
  "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
  }
}
typescript
await poststack.domains.cancelDkimRotation(42);
bash
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 numeric id — both are accepted.

Response fields

rotationobject
As in Start a rotation, back to state: "idle".

Status codes

StatusMeaning
200Cancelled.
401Missing, invalid or revoked API key or access token.
403insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:manage. Also returned when the team is suspended.
404Domain not found — no domain with this ID belongs to your team.
409No DKIM rotation is in progress for this domain.
429More 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#

POST
/domains/:id/ip

Sends this domain's mail from one of your team's dedicated IPs. Replaces any IP already assigned.

json
{
  "ipAddressId": 7
}
json
{
  "success": true
}
typescript
await poststack.domains.assignIp(42, 7);
bash
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 numeric id — both are accepted.

Body parameters

ipAddressIdintegerrequired
The ID of a dedicated IP assigned to your team.

Response fields

successboolean
Always true.

Status codes

StatusMeaning
200Assigned.
400ipAddressId is missing or not a positive integer.
401Missing, invalid or revoked API key or access token.
403insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:manage. Also returned when the team is suspended.
404Domain not found, or IP address not found or not assigned to your team.
409Stream mismatch: the domain's stream_preference and the IP's stream differ and neither is any.
429More than 10 calls a minute with the same credential. Wait for the number of seconds in Retry-After.

Remove the dedicated IP#

DELETE
/domains/:id/ip

Moves the domain back to the shared pool. Succeeds even if no IP was assigned.

json
{
  "success": true
}
typescript
await poststack.domains.unassignIp(42);
bash
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 numeric id — both are accepted.

Response fields

successboolean
Always true.

Status codes

StatusMeaning
200Unassigned.
401Missing, invalid or revoked API key or access token.
403insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:manage. Also returned when the team is suspended.
404Domain not found — no domain with this ID belongs to your team.
429More 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#

GET
/domains/:id/dmarc/reports

The aggregate reports received for the domain, newest first.

json
{
  "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 }
}
bash
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 numeric id — both are accepted.

Query parameters

pageintegerdefault 1
Page number, from 1.
perPageintegerdefault 20
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.
paginationobject
page, perPage, total, totalPages.

Status codes

StatusMeaning
200The page of reports.
400page or perPage is not a positive integer.
401Missing, invalid or revoked API key or access token.
403insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended.
404Domain not found — no domain with this ID belongs to your team.

Get DMARC stats#

GET
/domains/:id/dmarc/stats

Totals over the window plus one row per day that had reports. Cached for 5 minutes.

json
{
  "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 }
  ]
}
typescript
const stats = await poststack.domains.getDmarcStats(42, 30);
bash
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 numeric id — both are accepted.

Query parameters

daysintegerdefault 30
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

StatusMeaning
200The stats.
400days is not a whole number from 1 to 90.
401Missing, invalid or revoked API key or access token.
403insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended.
404Domain not found — no domain with this ID belongs to your team.

List DMARC sources#

GET
/domains/:id/dmarc/sources

Every 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.

json
{
  "sources": [
    { "sourceIp": "198.51.100.1", "total": 12500, "passed": 12500, "failed": 0 },
    { "sourceIp": "203.0.113.50", "total": 5, "passed": 0, "failed": 5 }
  ]
}
typescript
const { sources } = await poststack.domains.getDmarcSources(42, 30);
bash
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 numeric id — both are accepted.

Query parameters

daysintegerdefault 30
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

StatusMeaning
200The sources.
400days is not a whole number from 1 to 90.
401Missing, invalid or revoked API key or access token.
403insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended.
404Domain not found — no domain with this ID belongs to your team.

DMARC results by provider#

GET
/domains/:id/dmarc/providers

The same results grouped by the organisation that reported them, busiest first — so you can see that mail passes at Gmail but fails at Yahoo.

json
{
  "providers": [
    { "provider": "google.com", "total": 38200, "passed": 38150, "spfPassed": 38090, "dkimPassed": 38148 },
    { "provider": "Yahoo", "total": 4100, "passed": 4012, "spfPassed": 4003, "dkimPassed": 4011 }
  ]
}
bash
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 numeric id — both are accepted.

Query parameters

daysintegerdefault 30
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

StatusMeaning
200The breakdown.
400days is not a whole number from 1 to 90.
401Missing, invalid or revoked API key or access token.
403insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended.
404Domain 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#

GET
/domains/:id/placement

Per-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.

json
{
  "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 }
  ]
}
typescript
const placement = await poststack.domains.getInboxPlacement(42, 30);
bash
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 numeric id — both are accepted.

Query parameters

daysintegerdefault 30
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

StatusMeaning
200The summary.
400days is not a whole number from 1 to 90.
401Missing, invalid or revoked API key or access token.
403insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended.
404Domain not found — no domain with this ID belongs to your team.

Get mailbox-provider signals#

GET
/domains/:id/deliverability-signals

The 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.

json
{
  "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
}
bash
curl https://api.poststack.dev/domains/42/deliverability-signals \
  -H "Authorization: Bearer sk_live_..."

Path parameters

idstringrequired
The domain's dom_… ID or its numeric id — both are accepted.

Response fields

domainIdinteger
The domain’s numeric ID.
signals[]object[]
The newest value per provider + source + metric, with valueNum, valueText and observedAt.
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

StatusMeaning
200The signals (possibly empty).
401Missing, invalid or revoked API key or access token.
403insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended.
404Domain not found — no domain with this ID belongs to your team.

Get the reputation score#

GET
/domains/:id/reputation

PostStack'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.

json
{
  "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"
  }
}
bash
curl https://api.poststack.dev/domains/42/reputation \
  -H "Authorization: Bearer sk_live_..."

Path parameters

idstringrequired
The domain's dom_… ID or its numeric id — both are accepted.

Response fields

reputationobject | null
null until a score has been calculated.
reputation.scoreinteger
0–100.
reputation.factorsobject
totalSent, bounceRate, complaintRate, openRate, clickRate, engagementRate (fractions, 0–1), blocklistClean, spfVerified, dkimVerified, dmarcVerified.
reputation.calculatedAtstring (ISO 8601)
When it was calculated.

Status codes

StatusMeaning
200The score.
401Missing, invalid or revoked API key or access token.
403insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended.
404Domain not found — no domain with this ID belongs to your team.

Get reputation history#

GET
/domains/:id/reputation/history

The last score of each UTC day, oldest first. Days with no calculation are omitted.

json
{
  "history": [
    { "date": "2026-09-27", "score": 91 },
    { "date": "2026-09-28", "score": 93 },
    { "date": "2026-09-29", "score": 94 }
  ]
}
bash
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 numeric id — both are accepted.

Query parameters

daysintegerdefault 30
How many days back, 1–365.

Response fields

history[]object[]
date (YYYY-MM-DD) and score.

Status codes

StatusMeaning
200The history.
400days is not an integer from 1 to 365.
401Missing, invalid or revoked API key or access token.
403insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended.
404Domain not found — no domain with this ID belongs to your team.

Get daily volume#

GET
/domains/:id/timeseries

Daily sent, delivered, bounced, opened and clicked counts for the domain's mail.

json
{
  "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 }
  ]
}
bash
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 numeric id — both are accepted.

Query parameters

daysintegerdefault 30
7, 30 or 90. Any other value from 1 to 365 is treated as 30.

Response fields

timeseries[]object[]
date, sent, delivered, bounced, opened, clicked.

Status codes

StatusMeaning
200The series.
400days is not an integer from 1 to 365.
401Missing, invalid or revoked API key or access token.
403insufficient_scope: the key has sending-only access, or the OAuth token lacks domains:read. Also returned when the team is suspended.
404Domain 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.

lastCheckCodeWhat it means
record_not_foundNothing is published at that name yet — or your DNS host has not saved it.
spf_merge_neededYou 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_recordsTwo v=spf1 records are published. Merge them into one.
dkim_key_mismatchA 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_dotA CNAME or MX target was entered without a trailing dot, so your host appended your domain to it.
points_elsewhereThe CNAME or MX points somewhere other than the expected target.
null_mxAn "MX 0 ." record (“this domain accepts no mail”) is published next to ours. Delete it.
mx_outrankedOur MX is published, but another MX has a lower priority number, so incoming mail goes there instead.
return_path_driftThe 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.

Next steps

Was this page helpful?

Related