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:

  1. 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."
  }
}
  1. Per-send from check. The domain of the from address must be one of your domains with status verified. The match is exact: orders@mail.acme.com requires mail.acme.com itself to be verified; a subdomain of a verified domain does not inherit verification. A mismatch is a 422 validation_error with param: "from".

Test-mode sends are exempt from both checks, so you can build your integration before DNS is set up.

Add and verify

1
Create the domain
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.

2
Publish the DNS records

Add each record at your DNS host exactly as returned:

RecordsPurpose
3 CNAMEs at <token>._domainkey.mail.acme.comDKIM signing
MX at send.mail.acme.com, priority 10Custom MAIL FROM
TXT at send.mail.acme.comSPF for the MAIL FROM subdomain
TXT at _dmarc.mail.acme.comDMARC policy, starting at p=none (monitor-only)

If you lose the response, fetch the same records any time with GET /v1/domains/{id}.

3
Verify

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:

statusMeaning
pendingWithin the verification window; records not yet confirmed
partially_verifiedExactly one of DKIM and MAIL FROM has verified
verifiedDKIM and MAIL FROM both verified. The domain can send live email
failedThe 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 failed and the domain finalizes as failed, or partially_verified if exactly one record made it.
  • Re-verification is always allowed. Every verify call reads SES's latest check, so a domain that finalized as failed or partially_verified still flips to verified on 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).