Domain Verification
Verify your domain ownership through DNS record validation
After configuring DNS records, trigger verification to prove domain ownership and unlock email sending. MailLand checks SPF, DKIM, DMARC, and return path records.
Verify a Domain
POST https://api.mail.land/v1/domains/{id}/verifyAuthenticate with Authorization: Bearer <token> or X-API-Key: <api key>.
curl -X POST https://api.mail.land/v1/domains/k3v9x2m8q1w7z4/verify \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Response
{
"domainId": "k3v9x2m8q1w7z4",
"domain": "yourdomain.com",
"isVerified": true,
"spfStatus": "valid",
"dkimStatus": "valid",
"dmarcStatus": "valid",
"verifiedAt": "2026-02-17T10:15:00Z",
"message": "Domain successfully verified with all DNS records valid",
"dnsRecords": {
"spfRecord": "v=spf1 include:spf.mlnd.net ~all",
"dkimSelector": "mlnd",
"dkimRecord": "v=DKIM1; k=rsa; p=MIIBIjANBgkq...",
"dmarcRecord": "v=DMARC1; p=none; rua=mailto:[email protected]"
},
"trackingVerified": false,
"bounceVerified": true,
"bounceVerifiedAt": "2026-02-17T10:15:00Z",
"mxVerified": false,
"lastDnsCheckAt": "2026-02-17T10:15:00Z"
}| Field | Type | Description |
|---|---|---|
domainId | string | Domain public ID |
domain | string | Domain name |
isVerified | boolean | true only when SPF, DKIM and DMARC are all valid |
spfStatus, dkimStatus, dmarcStatus | enum | Record status: pending, valid or invalid |
verifiedAt | datetime | When the domain was verified |
message | string | Summary of the result |
dnsRecords | object | The SPF, DKIM (with dkimSelector) and DMARC records found in DNS, or the expected value when none was found |
spfErrorMessage, dkimErrorMessage, dmarcErrorMessage | string | Why the record failed; omitted when it passed |
trackingVerified, bounceVerified, mxVerified | boolean | Whether the custom tracking domain, the custom bounce domain (Return-Path) and the inbound MX record are verified |
trackingVerifiedAt, bounceVerifiedAt, mxVerifiedAt | datetime | When each of them was verified |
trackingErrorMessage, bounceErrorMessage, mxErrorMessage | string | Why one of them failed; returned only for the ones you have turned on |
lastDnsCheckAt | datetime | When DNS was last checked |
Record Statuses
Each DNS record check returns one of these statuses:
| Status | Description |
|---|---|
pending | Not checked yet |
valid | Record found and correct |
invalid | Record missing or incorrect |
Verification Requirements
For a domain to be verified (isVerified: true), SPF, DKIM and DMARC must all be valid:
| Record | Required | Must Pass |
|---|---|---|
| SPF | Yes | include:spf.mlnd.net must be present |
| DKIM | Yes | Valid public key for the active DKIM key |
| DMARC | Yes | A record at _dmarc that starts with v=DMARC1 and has a policy (p=none, p=quarantine or p=reject) |
| Return Path | No (on by default for new domains; does not block sending) | CNAME pointing to bounce.mlnd.net, DNS-only (not proxied) |
| Tracking | No (only with a custom tracking domain) | CNAME pointing to track.mlnd.net, DNS-only (not proxied) |
| Inbound MX | No (only with inbound email) | MX pointing to mx.mlnd.net |
The optional records do not change isVerified. A custom tracking or bounce domain is used for sending only after its own record is verified; until then, MailLand's default hosts are used.
Partial Verification
If some records are found but others are missing, the response shows exactly what needs attention:
{
"domainId": "k3v9x2m8q1w7z4",
"domain": "yourdomain.com",
"isVerified": false,
"spfStatus": "valid",
"dkimStatus": "invalid",
"dmarcStatus": "invalid",
"message": "Domain verification incomplete. Please check DNS records below.",
"dnsRecords": {
"spfRecord": "v=spf1 include:spf.mlnd.net ~all",
"dkimSelector": "mlnd",
"dkimRecord": "v=DKIM1; k=rsa; p=MIIBIjANBgkq...",
"dmarcRecord": "v=DMARC1; p=quarantine; pct=100; adkim=r; aspf=r; rua=mailto:[email protected]; sp=none"
},
"dkimErrorMessage": "DKIM record not found. Please add the TXT record to your DNS.",
"dmarcErrorMessage": "DMARC record not found. Please add TXT record at _dmarc subdomain.",
"trackingVerified": false,
"trackingErrorMessage": "CNAME record not found. Please add a CNAME record for track.yourdomain.com pointing to track.mlnd.net",
"bounceVerified": false,
"mxVerified": false,
"lastDnsCheckAt": "2026-02-17T10:20:00Z"
}Automatic Re-verification
MailLand periodically re-checks verified domains in the background via the PendingDomainVerificationWorker and DnsHealthCheckWorker:
- Pending domains: re-checked every hour for 7 days after they are added; after that, verify manually
- Verified domains: DNS health checked periodically to detect removed or changed records
If a verified domain's DNS records are later removed or changed, the affected record's status (spfStatus/dkimStatus/dmarcStatus) becomes invalid, isFullyVerified becomes false and sending stops until the record is fixed. verificationStatus itself stays Verified: MailLand restores the record's status automatically when DNS is correct again.
Domain Health Check
Run an on-demand health check that goes beyond basic verification:
POST https://api.mail.land/v1/domains/{id}/health-checkcurl -X POST https://api.mail.land/v1/domains/k3v9x2m8q1w7z4/health-check \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."{
"domainName": "yourdomain.com",
"status": "warning",
"previousStatus": "healthy",
"issues": [
{
"code": "DmarcNonePolicy",
"message": "DMARC policy is set to 'none' (monitoring only)",
"severity": "warning",
"record": "DMARC",
"details": "Consider upgrading to p=quarantine or p=reject"
}
],
"statusChanged": true,
"checkedAt": "2026-02-17T10:30:00Z",
"observationId": { "value": "3f2c9a1e-7b4d-4c1a-9e0f-5a6b7c8d9e0f" },
"spf": {
"lookupCompleted": true,
"isValid": true,
"value": "v=spf1 include:spf.mlnd.net ~all",
"issues": [],
"recordVerdict": "valid"
},
"dkim": {
"lookupCompleted": true,
"isValid": true,
"issues": [],
"recordVerdict": "valid"
},
"dmarc": {
"lookupCompleted": true,
"isValid": true,
"value": "v=DMARC1; p=none; rua=mailto:[email protected]",
"issues": [
{
"code": "DmarcNonePolicy",
"message": "DMARC policy is set to 'none' (monitoring only)",
"severity": "warning",
"record": "DMARC",
"details": "Consider upgrading to p=quarantine or p=reject"
}
],
"recordVerdict": "valid"
}
}Health Statuses
| Status | Description |
|---|---|
healthy | All checks pass: no issues detected |
warning | Domain works but there are potential improvements |
critical | Domain has issues that may prevent email delivery |
unknown | Unable to determine status (DNS lookup failure) |
Issue Codes
| Code | Severity | Description |
|---|---|---|
DnsNxDomain | critical | The domain does not exist (confirmed by a second, independent resolver) |
SpfMissing | critical | No SPF record found |
SpfMissingInclude | critical | The SPF record does not include spf.mlnd.net |
SpfPermError | critical | More than one SPF record (a permanent SPF error) |
SpfTooManyLookups | warning | The SPF record needs more than 10 DNS lookups |
DkimMissing | critical | No DKIM record found |
DkimMissingSelector | critical | No DKIM record at an active key's selector |
DkimKeyMismatch | critical | The DKIM key in DNS does not match MailLand's key |
DkimRevoked | critical | The DKIM key has been revoked (empty p= tag) |
DmarcMissing | warning | No DMARC record found |
DmarcNonePolicy | warning | DMARC policy is none (monitoring only) |
MxMissing | critical | No MX record (checked only when inbound email is on) |
MxWrongHost | warning | The MX record does not point to mx.mlnd.net |
DnsTimeout, DnsServFail, DnsLookupFailed | warning | The DNS lookup did not complete; nothing was concluded about the record |
Health History
View historical health check results. Optional from/to query parameters (ISO 8601 dates) narrow the range:
GET https://api.mail.land/v1/domains/{id}/health-historycurl https://api.mail.land/v1/domains/k3v9x2m8q1w7z4/health-history \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."[
{
"domainName": "yourdomain.com",
"prevStatus": "healthy",
"newStatus": "warning",
"issues": ["DmarcNonePolicy"],
"reason": "Scheduled health check",
"checkedAt": "2026-02-17T10:30:00Z"
},
{
"domainName": "yourdomain.com",
"prevStatus": "warning",
"newStatus": "healthy",
"issues": [],
"reason": "Scheduled health check",
"checkedAt": "2026-02-16T10:30:00Z"
}
]The response is a plain array (no items wrapper, no nextCursor/hasMore).
Troubleshooting
SPF record not found
- Ensure you have exactly one SPF TXT record on your root domain
- Multiple SPF records cause failures: merge them into a single record
- Check that
include:spf.mlnd.netappears before the~allor-allmechanism
DKIM record not found
- Verify the record is at
mlnd._domainkey.yourdomain.com(notmlnd._domainkey) - The record type must be TXT (not CNAME)
- Some DNS providers require quotes around long TXT values
- Check for trailing spaces or newlines in the pasted public key
Return path CNAME not found
- Confirm the CNAME is at
bounce.yourdomain.com(or your custom bounce subdomain) - The CNAME value must be
bounce.mlnd.net(not an IP address, and not an MX record) - Cloudflare users: ensure the proxy is disabled (grey cloud / DNS only)
DNS propagation delay
- Most DNS changes propagate within 5–15 minutes
- If using a low TTL (300s), changes should appear quickly
- Existing records with high TTL may take up to 48 hours to update globally
- Try verifying again after waiting 15 minutes
Next Steps
- DNS Setup: review the required DNS records
- DKIM Key Rotation: rotate DKIM signing keys
- REST API: start sending email after verification