04 · MailMarco API
Domains
A sending domain is five DNS records and one verification call. Until they all resolve, every send from that domain is a 400.
Domain lifecycle
POST /v1/domains POST /v1/domains/{id}/verify
│ │
▼ ▼
pending ───────────────────► verified ──── POST …/suspend ──► suspended
│ all required ▲
│ records resolve │ a later verify that passes again
│ │
└── some but not all resolve ──► failed| Status | Meaning |
|---|---|
pending | Created. Either nothing has been published yet, or a verification attempt resolved no records at all — a run where nothing resolves deliberately leaves the domain pending rather than marking it failed. |
failed | A verification run resolved some records but not every required one. Fix the missing records and verify again. |
verified | Every required record matched. Sending from this domain is allowed. `verifiedAt` is set once and never overwritten by later runs. |
suspended | Suspended via the API or by an operator. |
verifying | Reserved in the status contract; the API does not emit it today. |
Add a domain
/v1/domainsRegisters the domain, generates a fresh DKIM keypair, and returns the complete record set to publish.
Body
domainstringrequired | Trimmed and lower-cased. Must be a dotted name of a-z0-9- labels (1–63 characters each) with no leading or trailing hyphen — mail.example.com is valid, example and -bad.example.com are not. |
curl -X POST https://api.mailmarco.com/v1/domains \
-H "authorization: Bearer $MAILMARCO_ACCESS_TOKEN" \
-H "content-type: application/json" \
-d '{"domain":"mail.example.com"}'{
"id": "3c2b1a09-8d7e-4c5b-a6f2-1e0d9c8b7a65",
"domain": "mail.example.com",
"status": "pending",
"region": "eu",
"verifiedAt": null,
"createdAt": "2026-07-29T09:16:41.882Z",
"records": [
{ "id": "…", "type": "SPF", "host": "mail.example.com", "value": "v=spf1 include:spf.mailmarco.com ~all", "ttl": 3600, "verified": false, "checkedAt": null },
{ "id": "…", "type": "DKIM", "host": "mm20260729a1b2c3._domainkey.mail.example.com", "value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A…", "ttl": 3600, "verified": false, "checkedAt": null },
{ "id": "…", "type": "DMARC", "host": "_dmarc.mail.example.com", "value": "v=DMARC1; p=quarantine; rua=mailto:[email protected]; fo=1", "ttl": 3600, "verified": false, "checkedAt": null },
{ "id": "…", "type": "RETURN_PATH", "host": "bounce.mail.example.com", "value": "bounce.mailmarco.com", "ttl": 3600, "verified": false, "checkedAt": null },
{ "id": "…", "type": "TRACKING", "host": "track.mail.example.com", "value": "track.mailmarco.com", "ttl": 3600, "verified": false, "checkedAt": null }
]
}Errors
400— The domain does not match the accepted grammar.409— The domain is already registered.
The generated DNS records
All five are required — verification only succeeds when every one of them resolves.
| Type | RR | Host | What it does |
|---|---|---|---|
SPF | TXT | example.com | Authorises MailMarco's sending IPs via include:spf.mailmarco.com. Published with ~all (softfail) so an unrelated sender is marked, not hard-rejected. If you already have an SPF record on that name, merge the include into it — a domain must publish exactly one SPF record. |
DKIM | TXT | <selector>._domainkey.example.com | Publishes the RSA public key for the keypair minted for this domain. This is the cryptographic proof of ownership and the record that survives forwarding — the one to get right first. |
DMARC | TXT | _dmarc.example.com | Policy p=quarantine with fo=1, and aggregate reports sent to a MailMarco-operated mailbox so failures are actually monitored. Tighten to p=reject yourself once you are confident. |
RETURN_PATH | CNAME | bounce.example.com | Aligns the envelope return-path with your domain so bounces are attributable and SPF aligns. Points at bounce.mailmarco.com, where the MX that receives bounce mail actually lives. |
TRACKING | CNAME | track.example.com | Serves open pixels and click redirects from your own hostname instead of a shared one. See Tracking. |
As a zone file
; substitute your own selector and public key from the API response
mail.example.com. 3600 IN TXT "v=spf1 include:spf.mailmarco.com ~all"
mm20260729a1b2c3._domainkey.mail.example.com. 3600 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkq…"
_dmarc.mail.example.com. 3600 IN TXT "v=DMARC1; p=quarantine; rua=mailto:[email protected]; fo=1"
bounce.mail.example.com. 3600 IN CNAME bounce.mailmarco.com.
track.mail.example.com. 3600 IN CNAME track.mailmarco.com.Verification
/v1/domains/{id}/verifyResolves every record over live DNS, stores each result, and moves the domain status accordingly.
Path parameters
idstring (uuid)required | Domain id. |
curl -X POST https://api.mailmarco.com/v1/domains/$DOMAIN_ID/verify \
-H "authorization: Bearer $MAILMARCO_ACCESS_TOKEN"{
"id": "3c2b1a09-8d7e-4c5b-a6f2-1e0d9c8b7a65",
"domain": "mail.example.com",
"status": "verified",
"region": "eu",
"verifiedAt": "2026-07-29T10:02:55.410Z",
"createdAt": "2026-07-29T09:16:41.882Z",
"records": [
{ "id": "…", "type": "SPF", "host": "mail.example.com", "value": "v=spf1 include:spf.mailmarco.com ~all", "ttl": 3600, "verified": true, "checkedAt": "2026-07-29T10:02:55.402Z" }
]
}Errors
404— No domain with that id in your organization.429— More than 10 verification calls for this organization in an hour. The body carriesretryAfterSeconds.
How each record is checked
- TXT — every answer's chunks are joined back together and compared for an exact match against the expected value. A record split across strings by your provider is fine; a modified value is not.
- CNAME — compared case-insensitively, ignoring a trailing dot.
- Failures are not distinguished.
NXDOMAIN,NODATAandSERVFAILall read as “not published yet”, so a broken resolver looks the same as an unpublished record.
A verification run that flips the domain into verified for the first time emits a domain.verified event, which you can subscribe to with a webhook. Re-verifying an already-verified domain does not emit it again.
Suspending a domain
/v1/domains/{id}/suspendMarks the domain suspended. Sends from it are refused with 400 until it is verified again.
Path parameters
idstring (uuid)required | Domain id. |
{
"id": "3c2b1a09-8d7e-4c5b-a6f2-1e0d9c8b7a65",
"domain": "mail.example.com",
"status": "suspended",
"region": "eu",
"verifiedAt": "2026-07-29T10:02:55.410Z",
"createdAt": "2026-07-29T09:16:41.882Z"
}Errors
404— No domain with that id in your organization.
Endpoints
/v1/domainsAll domains in the organization, without their DNS records.
/v1/domains/{id}One domain, with its DNS records and their latest check results.
Path parameters
idstring (uuid)required | Domain id. |
Errors
404— No domain with that id in your organization.
/v1/domains/{id}/dns-recordsJust the records — the array you would render in a setup wizard.
Path parameters
idstring (uuid)required | Domain id. |
[
{
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"type": "DKIM",
"host": "mm20260729a1b2c3._domainkey.mail.example.com",
"value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A…",
"ttl": 3600,
"verified": true,
"checkedAt": "2026-07-29T10:02:55.402Z"
}
]Responses never include the organization id or any DKIM private-key material — the DTO mappers strip both before serialisation.