Sending
Sending domains
We sign your mail as your domain, so you publish one record proving we are allowed to. Sending from an unverified domain is refused outright rather than queued and failed later.
What you publish
One record is required. The other two are returned for completeness and one of them is actively dangerous to paste over an existing value.
| Record | Status | Why |
|---|---|---|
| s1._domainkey | required | The DKIM public key. The only record we require — DMARC passes when either SPF or DKIM aligns, and we sign as your domain, so DKIM alignment alone is sufficient. The value is longer than a single 255-character DNS string, so it is also returned pre-split as chunks for providers that need that. |
| _dmarc | optional recommended | Your policy for your own domain. Start at p=none and read two weeks of reports before tightening — going straight to p=reject silently blackholes legitimate mail. Only add it if you have none already. |
| SPF | optional not needed | Our envelope sender is on our own bounce domain, so SPF is evaluated against us, not you. Your SPF record is never consulted for our mail. |
Do not replace an existing SPF record. A domain may publish only one, and overwriting yours would break every other system that sends as you — your CRM, your invoicing, your helpdesk. Verification never asks for SPF and never fails because of it.
Add a domain
/v1/domainsdomains:writeGenerates a fresh 2048-bit DKIM key pair for this domain and returns the records to publish. The private half is encrypted at rest and never leaves the platform.
| Field | Type | Notes |
|---|---|---|
| name | string required | 3–253 characters, and must look like a domain — at least one dot, labels of a–z 0–9 - not starting or ending with a hyphen. Lower-cased on the way in. |
const domain = await posthaste.domains.create({ name: 'yourdomain.com' })
domain.id // 'dom_9pXbNc7HrQ8T2Sf3kQAZLm'
domain.status // 'pending'
// One required record: the DKIM key. SPF and DMARC come back too, marked
// optional, with a warning apiece about overwriting an existing record.
const dkim = domain.records.find((r) => r.required)
console.log(dkim.type, dkim.name, dkim.value)| Status | When |
|---|---|
| 201 | Created. Publish the records. |
| 409 conflict | That domain is already on this account. Re-adding it consumes no new slot; fetch it from the list instead. |
| 422 domain_limit_reached | Your plan includes fewer domains than you are trying to use. The body carries limit and used. Fixed by upgrading, not by retrying. |
| 400 invalid_request | The name is not a domain. |
Verify it
/v1/domains/:id/verifydomains:writeLooks the records up in DNS now and records the outcome. It asks the zone’s own nameservers first and falls back to public resolvers, so no intermediate cache gets to decide whether you can send — a negative cache entry has previously reported a correctly published record as missing.
Every check is reported, passing or not, so you can see exactly which record is wrong instead of being told “verification failed”. Only checks with required: true gate the result.
const result = await posthaste.domains.verify('dom_9pXbNc7HrQ8T2Sf3kQAZLm')
// Branch on `verified`, not on the checks: SPF and DMARC are reported but
// neither of them failing stops the domain being usable.
if (!result.verified) {
for (const check of result.checks) {
console.log(check.record, check.status, check.detail)
}
}| Field | Notes |
|---|---|
| status | verified or failed. |
| verified | Boolean form of the same answer. This is the one to branch on. |
| checks[].record | dkim, spf or dmarc. |
| checks[].status | pass, fail (found, wrong) or not_found (nothing published). |
| checks[].detail | Plain-language explanation, safe to show a customer verbatim. |
A fresh record is often just not visible yet. DNS propagation takes a minute or two. A not_found immediately after publishing is normal — wait and call this again rather than re-publishing.
| Status | When |
|---|---|
| 200 | Verification ran. Read `verified`, not the status code. |
| 404 not_found | No such domain on this account. |
List domains
/v1/domainsdomains:readNewest first, not paginated — an account has a handful of domains. Each row carries the records to publish, so a browser tab closed before the DKIM value was copied is not a dead end. Everything returned is derived from stored public data; no private key is involved.
const { data } = await posthaste.domains.list()
for (const domain of data) {
domain.status // 'pending' | 'verified' | 'failed' | 'disabled'
domain.messagesSent // non-zero is what makes deleting it refused
domain.records // on the LIST too, so closing that first tab loses nothing
}| Field | Notes |
|---|---|
| status | pending, verified, failed or disabled. Only verified may send. |
| selector | Which key signs this domain’s mail, so a rotation is visible. |
| messagesSent | Messages ever sent from it — and what makes deleting it refused. |
| lastCheckedAt | When the record was last looked for by the background sweep, so pending is not a mystery. |
Remove a domain
/v1/domains/:iddomains:writeA domain that has never sent is deleted outright. One that has is refused, and that is a deliberate choice rather than a limitation: the delivery record is the product, and removing the domain would take its history with it. Stop sending from it instead — an unused domain costs nothing and keeps its records.
Inbound addresses on the domain go with it when the delete succeeds.
| Status | When |
|---|---|
| 204 | Deleted. |
| 409 domain_in_use | The domain has messages on record. The body carries messageCount. |
| 404 not_found | No such domain, or the id is not a domain id. |
Setup guidance
/v1/domains/:id/setupdomains:readAnswers “who hosts this domain’s DNS, and can we do this for them in one click”. It is what the dashboard’s setup screen calls so it can offer the right path instead of showing everybody a token field. Two live DNS lookups, so expect it to take a moment.
| Field | Notes |
|---|---|
| host | The identified DNS provider — id, name, url, note and the first four nameservers — or null if it could not be identified. |
| oneClick | { provider, url } when the provider supports Domain Connect and the template is available, otherwise null. Send the customer to url; they approve the change at their provider. When it is null, show them the manual records — a button that leads to a dead end is worse than no button. |
Publish DKIM through Cloudflare
/v1/domains/:id/cloudflaredomains:writeIf the domain is on Cloudflare, hand us a scoped API token and we will write the DKIM record for you. Exactly one record is written — SPF and DMARC are left alone on purpose, for the reason above.
| Field | Type | Notes |
|---|---|---|
| token | string optional | A Cloudflare API token with Zone:DNS:Edit on this zone, 20–200 characters. May be omitted if you previously stored one with remember. |
| remember | boolean optional | Default false. Stores the token, encrypted at rest under the same envelope as a DKIM private key, so adding a second domain needs no paste. Only honoured when a token was supplied on this request. |
The token is verified before anything is stored, the zone is located by name, the record is written, and then normal verification runs. A response with verified: false usually just means propagation has not caught up.
| Field | Notes |
|---|---|
| published | What was written: [{ type, name, created }]. created is false when an existing record was updated. |
| zone | The Cloudflare zone the record went into. |
| verified | Whether the record was already visible in DNS when we checked. |
| notPublished | The records we deliberately did not touch, each with a why — so the difference between “done for you” and “still yours to decide” is explicit. |
| Status | When |
|---|---|
| 200 | Written. Read `verified` for whether it is live yet. |
| 400 token_required | No token supplied and none stored. |
| 400 cloudflare_token_invalid | Cloudflare rejected the token. |
| 400 cloudflare_zone_not_found | The token is valid but does not cover this zone. |
| 400 cloudflare_write_failed | The write was refused — usually the token lacks Zone:DNS:Edit. |
| 404 not_found | No such domain on this account. |
Forget a stored token
/v1/account/cloudflaredomains:writeDeletes the stored Cloudflare credential for the account. Always 204, whether one was stored or not — it is a statement about the end state, and reporting “there was nothing to delete” would tell a caller something about the account they may not have known.
NextSend an email →Every field on POST /v1/emails, including replyTo and List-Unsubscribe.