Verify a domain
Add a sending domain, publish its DNS records, and verify it before sending live email.
Mailfully sends from domains you own. Before live sends are accepted, you add a domain, publish six DNS records, and verify it.
Why verification is required
Two checks run when the API accepts a live send:
- Org-level gate. If your organization has no verified domain, every live send is rejected with
403 unverified_domain_required:
{
"error": {
"type": "unverified_domain_required",
"message": "Your organization has no verified sending domain. Add and verify a domain before sending live email, or use test mode."
}
}
- Per-send from check. The domain of the
fromaddress must be one of your domains with statusverified. The match is exact:orders@mail.acme.comrequiresmail.acme.comitself to be verified; a subdomain of a verified domain does not inherit verification. A mismatch is a422 validation_errorwithparam: "from".
Test-mode sends are exempt from both checks, so you can build your integration before DNS is set up.
Add and verify
curl -X POST https://api.mailfully.com/v1/domains \
-H "Authorization: Bearer mf_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "name": "mail.acme.com" }'The 201 response includes the domain and the exact DNS records to publish:
{
"object": "domain",
"id": "dom_01J1PZ3M8Q7VXCK4T2R9WFH6BD",
"name": "mail.acme.com",
"region": "us-east-1",
"mail_from_subdomain": "send.mail.acme.com",
"dkim_tokens": [
"6gbrjpgwjskckoa6a5zn6fwqkn67xbtw",
"yybhvng4tqglsubhkkbwfwbtvvxjb5lb",
"wrx2s7fgbfyvfh2c3wch5bqk3gpjrmtp"
],
"dkim_status": "not_started",
"mail_from_status": "not_started",
"dmarc_status": "not_started",
"tracking_status": "not_started",
"tracking_enabled": false,
"config_set": "mf-org_01J1PZ0R2NQ8XWJH3KTV6C5FYD-dom_01J1PZ3M8Q7VXCK4T2R9WFH6BD",
"status": "pending",
"created_at": "2026-07-08T14:05:02.318Z",
"records": [
{ "type": "CNAME", "name": "6gbrjpgwjskckoa6a5zn6fwqkn67xbtw._domainkey.mail.acme.com", "value": "6gbrjpgwjskckoa6a5zn6fwqkn67xbtw.dkim.amazonses.com" },
{ "type": "CNAME", "name": "yybhvng4tqglsubhkkbwfwbtvvxjb5lb._domainkey.mail.acme.com", "value": "yybhvng4tqglsubhkkbwfwbtvvxjb5lb.dkim.amazonses.com" },
{ "type": "CNAME", "name": "wrx2s7fgbfyvfh2c3wch5bqk3gpjrmtp._domainkey.mail.acme.com", "value": "wrx2s7fgbfyvfh2c3wch5bqk3gpjrmtp.dkim.amazonses.com" },
{ "type": "MX", "name": "send.mail.acme.com", "value": "feedback-smtp.us-east-1.amazonses.com", "priority": 10 },
{ "type": "TXT", "name": "send.mail.acme.com", "value": "v=spf1 include:amazonses.com ~all" },
{ "type": "TXT", "name": "_dmarc.mail.acme.com", "value": "v=DMARC1; p=none;" }
]
}The optional mail_from_prefix field controls the MAIL FROM subdomain; it defaults to send, so the MX and SPF records land on send.mail.acme.com.
Add each record at your DNS host exactly as returned:
| Records | Purpose |
|---|---|
3 CNAMEs at <token>._domainkey.mail.acme.com | DKIM signing |
MX at send.mail.acme.com, priority 10 | Custom MAIL FROM |
TXT at send.mail.acme.com | SPF for the MAIL FROM subdomain |
TXT at _dmarc.mail.acme.com | DMARC policy, starting at p=none (monitor-only) |
If you lose the response, fetch the same records any time with GET /v1/domains/{id}.
Amazon SES checks your published DNS on its own cadence; each call to the verify endpoint reads SES's latest recorded result and returns the updated domain:
curl -X POST https://api.mailfully.com/v1/domains/dom_01J1PZ3M8Q7VXCK4T2R9WFH6BD/verify \
-H "Authorization: Bearer mf_live_xxxxxxxxxxxx"A 200 response mid-verification, with MAIL FROM confirmed and DKIM still propagating:
{
"object": "domain",
"id": "dom_01J1PZ3M8Q7VXCK4T2R9WFH6BD",
"name": "mail.acme.com",
"region": "us-east-1",
"mail_from_subdomain": "send.mail.acme.com",
"dkim_tokens": [
"6gbrjpgwjskckoa6a5zn6fwqkn67xbtw",
"yybhvng4tqglsubhkkbwfwbtvvxjb5lb",
"wrx2s7fgbfyvfh2c3wch5bqk3gpjrmtp"
],
"dkim_status": "pending",
"mail_from_status": "verified",
"dmarc_status": "not_started",
"tracking_status": "not_started",
"tracking_enabled": false,
"config_set": "mf-org_01J1PZ0R2NQ8XWJH3KTV6C5FYD-dom_01J1PZ3M8Q7VXCK4T2R9WFH6BD",
"status": "partially_verified",
"created_at": "2026-07-08T14:05:02.318Z"
}Repeat the call until status is verified. Live sends from orders@mail.acme.com are accepted from that point on.
Verification statuses
The domain's status moves through four values:
status | Meaning |
|---|---|
pending | Within the verification window; records not yet confirmed |
partially_verified | Exactly one of DKIM and MAIL FROM has verified |
verified | DKIM and MAIL FROM both verified. The domain can send live email |
failed | The 72-hour window elapsed with neither DKIM nor MAIL FROM verified. If exactly one made it, the domain finalizes as partially_verified instead |
Per-record fields (dkim_status, mail_from_status) move through not_started, pending, verified, and failed. Verification gates on DKIM and MAIL FROM only. The DMARC record is advisory and never blocks the verified status.
Two rules govern the timeline:
- 72-hour window. The window starts when the domain is created. If it elapses before both records verify, each unverified record is marked
failedand the domain finalizes asfailed, orpartially_verifiedif exactly one record made it. - Re-verification is always allowed. Every verify call reads SES's latest check, so a domain that finalized as
failedorpartially_verifiedstill flips toverifiedon a later call once SES reports both records verified. Fix the records and verify again.
Open and click tracking
Tracking is a single boolean per domain that covers opens and clicks together. Enabling it points the domain's tracking links at a custom redirect subdomain, email.mail.acme.com, and returns one more CNAME to publish:
curl -X POST https://api.mailfully.com/v1/domains/dom_01J1PZ3M8Q7VXCK4T2R9WFH6BD/tracking \
-H "Authorization: Bearer mf_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "enabled": true }'The response returns the updated domain plus the record to add:
{
"records": [
{ "type": "CNAME", "name": "email.mail.acme.com", "value": "r.us-east-1.awstrack.me" }
]
}
Disabling with { "enabled": false } returns "records": [], and you can remove the CNAME. See POST /v1/domains/{id}/tracking for the full response shape.
Troubleshooting
DNS propagation can take anywhere from minutes to hours depending on your host and record TTLs. SES re-checks DNS on its own schedule and the verify endpoint reports its latest result, so a record that fails a check inside the 72-hour window is not final. Keep the records published and verify again. Sends do not bounce while the MX record propagates: until it resolves, mail falls back to the default MAIL FROM instead of failing.
The three DKIM records must be plain CNAMEs that resolve to their dkim.amazonses.com targets. If your DNS host proxies or flattens CNAME records, the published name may stop resolving to that target, so publish them as unproxied, DNS-only records. Also check for hosts that auto-append the zone name: if yours does, enter only the host part (<token>._domainkey) so the record doesn't end up at <token>._domainkey.mail.acme.com.mail.acme.com. Confirm with dig CNAME <token>._domainkey.mail.acme.com +short.
Mailfully's SPF TXT record lives on the MAIL FROM subdomain (send.mail.acme.com), not your root domain, so it does not conflict with an existing SPF record on your apex. Publish it as returned. If you already publish an SPF TXT on that exact subdomain, merge include:amazonses.com into the existing record instead (a single name must not carry two SPF records).
Related
POST /v1/domains— full request and response referencePOST /v1/domains/{id}/verify- Errors — the full error catalog, including the send-gate errors above