Verify your sending domain.
A sending domain needs one DNS record: its DKIM key. Publish it and Mail flips your domain to active automatically.
1
Add the domain
Console → Mail → Domains → Add domain. Enter the FQDN you want to send from (e.g. acme.com; the dialog also asks whether it's a root domain or a subdomain, and whether you want to send, receive, or both). Mail generates a fresh DKIM keypair and drops you on a per-domain setup checklist — one step per record, each with the exact host and value to paste at your registrar (they're also on the API as dns_instructions on every domain read).
# intent: send
TXT productcraft._domainkey.mail.acme.com v=DKIM1; k=rsa; p=MIIBIjANBgk...
MX bounce.mail.acme.com 10 feedback-smtp.eu-north-1.amazonses.com (recommended)
TXT bounce.mail.acme.com v=spf1 include:amazonses.com ~all (recommended)
# intent: both (adds inbound)
TXT productcraft._domainkey.mail.acme.com v=DKIM1; k=rsa; p=MIIBIjANBgk...
MX mail.acme.com 10 mx.productcraft.co
# intent: receive
MX mail.acme.com 10 mx.productcraft.co
TXT _productcraft-verify.mail.acme.com productcraft-verify=<token>Not sure what to add? GET .../domains/lookup?fqdn=acme.com creates nothing and tells you the zone, the DNS provider it detected from your nameservers (and whether it supports one-click Domain Connect setup), the SPF, DMARC and MX records already there, and, if you typed an apex, a subdomain to send from instead (mail.acme.com, or send.acme.com when mail. is already in use).
To hand the records to whoever manages DNS, or to import them in one go, download them: GET .../domains/:id/dns-records/export?format=bind gives a zone-file fragment that Cloudflare (DNS → Records → Import) and Route 53 (Import zone file) accept, and format=csv gives type,name,value,priority,ttl,purpose,required. Add &zone=acme.com to choose the zone the names are relative to, and &all=true to include records that already verify.
If someone else manages your DNS, send them a share link instead of a console invite: POST .../domains/:id/share-links with {"expires_in_days": 7, "email_to": ["it@acme.com"]} returns a url to a read-only setup page with the records, their live status and the downloads above, no account needed and nothing else about your workspace. It works for 1 to 7 days, is shown only once, and DELETE .../share-links/:link_id turns it off at once. email_to is optional (up to 5 recipients per domain per day); GET .../share-links lists your links with their status and when each was last opened.
If your DNS is on Cloudflare or DigitalOcean, Mail can add the records for you with a short-lived API token: POST .../domains/:id/dns-records/apply with {"provider": "digitalocean", "token": "...", "dry_run": true} and an Idempotency-Key header returns the plan (each record as create, update or skip) without changing anything; send it again with "dry_run": false to write exactly that plan. It only adds records at or under your domain, never deletes anything, and the only record it ever changes is an existing SPF record our mechanism must be merged into (shown with before and after). Unless you send "include_dmarc": false, it also adds v=DMARC1; p=none; at _dmarc when no DMARC policy covers the domain yet; an existing policy is never touched. Conflicts stop the whole apply with a 409 and the plan. Use a token that can do as little as possible and expires soon: on Cloudflare, an API token for this one zone with Zone → DNS → Edit; on DigitalOcean, a personal access token with the custom scopes domain:read, domain:create and domain:update and a 1-hour expiry. Mail uses it for that one request and never stores or logs it; revoke it when you are done (the response warns you when the token could see more than this zone).
The exact values are in the console after you create the domain. Don't copy them from here — the DKIM public key + verify token are per-domain.
A domain has an intent — send, receive, or both (the default) — which decides which records it is given and which must pass before it goes active. Each record in dns_instructions carries required: true|false. A send-only domain is never asked for an MX record, so adding your apex domain to send from does not touch its inbound mail. We recommend a subdomain such as mail.acme.com anyway, so your product mail's reputation stays separate from your team's.
2
Verify
You don't strictly have to do anything here — Mail re-checks pending domains every ~5 minutes and flips them to active on its own once the required records land. To check on demand, open the domain's setup checklist and hit Check DNS now, or call POST .../dns-records/check. Mail asks your zone's own nameservers and a public resolver for every record and returns one row per record with a state: missing, mismatch (with what it found), propagating (your nameservers serve it, public resolvers will shortly; the hint says roughly when) or verified. A failed DNS lookup never turns a record red. When something else at that name stops a record from working, its state is conflict and conflicts[] says why and how to fix it: a second SPF record (with the merged record to publish instead), an SPF record over the 10-lookup limit, a CNAME where a TXT or MX must go, a second MX on the MAIL FROM host, two DMARC records, or another DKIM key on the productcraft selector. Mail never asks you to add a second SPF record: if you already have one, merge. GET .../dns-records returns the last result without a lookup, and ?zone= renders each name_relative the way your DNS provider wants it. The older POST .../verify still works and answers 400 with a dns_check_results diff while a required record is missing.
curl -X POST https://api.mail.productcraft.co/v1/workspaces/<ws>/domains/<id>/dns-records/check \
-H 'authorization: Bearer pcft_live_...'
# → { "zone": "acme.com", "records": [ { "purpose": "dkim",
# "name_relative": "productcraft._domainkey.mail", "state": "propagating",
# "hint": "Your nameservers already serve it; public resolvers will within ~30 min ..." } ] }After verification: drift monitoring. A verified domain stays active, but Mail keeps checking it (hourly, and every 5 minutes while something is wrong). If a required record disappears or changes (the DKIM TXT, or the MX on a receiving domain), or DKIM signing stops verifying, the domain's health moves from ok to degraded: mail still flows, every workspace owner gets an email naming the record, and a domain.updated webhook fires with the per-record changes. If it is still broken 72 hours later (health_grace_ends_at on the domain says when), health becomes failing, owners are emailed again, and sends from the domain are refused with 422 domain_not_verified_by_provider rather than being accepted and dropped by the provider. Put the record back and the next check returns it to ok. A DNS lookup that fails (a timeout, SERVFAIL) never counts as drift, and losing a recommended record (MAIL FROM, DMARC) only sends domain.updated. Subscribe a webhook to domain.created, domain.verified, domain.updated and domain.deleted to follow all of this from your own systems.
3
Why these records?
- DKIM — cryptographic signature on every message. Lets Gmail / Outlook / corporate filters verify the message hasn't been tampered with and was authorised by your domain. Without it, your delivery rate tanks.
- DKIM is also the ownership proof. The key was generated for your domain alone, so a zone that serves it was edited by someone who could read it from your workspace. Sending domains therefore need no separate verification token.
- Verify token (receive-only domains) — proves to Mail that you control the domain when there is no DKIM key to publish. Not visible to recipients. You can remove it once the domain is active.
- MX (receive and both) — routes inbound mail on your domain to Mail (see below).
- MAIL FROM (recommended, sending domains) — an MX and an SPF TXT on
bounce.mail.acme.com. Bounces then return to a host under your domain and SPF aligns with your From address, on top of DKIM. Until you publish them, mail still flows with our default envelope sender. Change the label (or turn it off withnull) withPATCH .../domains/:id{"mail_from_subdomain": "bounce"}; a label whose host already has other records is refused with 409. - SPF on your From domain — no longer requested. Domains added before October 2026 were asked for
v=spf1 include:_spf.productcraft.co -all; keep it if you have it (it is still reported, as informational), but it never blocks verification.
4
DMARC (recommended, not required)
DMARC is the policy layer that tells receivers what to do when SPF or DKIM fail. Gmail and Yahoo require it from bulk senders. Mail looks for a policy at _dmarc.mail.acme.com and then at each parent (_dmarc.acme.com), grades whichever applies in the domain's dmarc_grade (policy, sp=, alignment, pct=, rua=, plus a warning when something needs attention), and only if no policy applies does it recommend one, scoped to the sending domain and never your organisation's:
TXT _dmarc.mail.acme.com v=DMARC1; p=none;It never asks for a second DMARC record and never touches an existing one. If your team manages DMARC elsewhere, hide the recommendation with PATCH .../domains/:id {"dmarc_managed": false}; the grade is still shown. Once the aggregate reports at rua= look clean, tighten to p=quarantine, then p=reject.
5
Inbound — the MX record
The MX record is what makes mail to your domain (support@, replies, etc.) land in Mail mailboxes. If you only ever want to send, create the domain with intent: "send" and skip it — outbound-only.
Inbound mail hits mx.productcraft.co → parsed + stored under the matching mailbox. Listable via GET /v1/workspaces/:ws/mailboxes/:id/messages.
6
Brand
Optional: workspace-wide design tokens your templates can use — name, logo, colours, font stack, footer. They're addressable in any template as {{brand.*}} and drive the built-in layout + footer partials, so restyling every email is one API call. All fields are optional; unset fields fall back to platform defaults (GET returns the resolved set, DELETE resets to defaults).
curl -X PUT https://api.mail.productcraft.co/v1/workspaces/<ws>/brand \
-H 'authorization: Bearer pcft_live_...' \
-H 'content-type: application/json' \
-d '{
"brand_name": "Acme",
"logo_url": "https://cdn.acme.com/logo.png",
"primary_color": "#1E40AF"
}'More knobs — secondary/text/background colours, font_stack, button_radius, footer_html, footer_address, social_links — are in the API reference.
7
Reverse DNS (PTR)
One subtle thing ProductCraft handles, not you: every IP we send from keeps matching forward + reverse DNS (PTR). Mentioning so you know it's in the path — without a PTR, Gmail silently spam-filters outbound mail.