domains.md
title: "Domains" description: "Add custom domains to MachineMail, get their DNS records, zone file and Domain Connect setup, verify them and turn on catch-all, for the organization or a pod." canonical: "https://machinemail.dev/docs/reference/api/domains"
Domains
Add custom domains to MachineMail, get their DNS records, zone file and Domain Connect setup, verify them and turn on catch-all, for the organization or a pod.
Source: https://machinemail.dev/docs/reference/api/domains
| Method | Path | Summary |
|---|---|---|
GET |
/v0/domains |
List Domains |
POST |
/v0/domains |
Create Domain |
GET |
/v0/domains/{domain_id} |
Get Domain |
PATCH |
/v0/domains/{domain_id} |
Update Domain |
DELETE |
/v0/domains/{domain_id} |
Delete Domain |
GET |
/v0/domains/{domain_id}/domain-connect |
Get Domain Connect Setup |
POST |
/v0/domains/{domain_id}/verify |
Verify Domain |
GET |
/v0/domains/{domain_id}/zone-file |
Get Zone File |
GET |
/v0/pods/{pod_id}/domains |
List Domains in Pod |
POST |
/v0/pods/{pod_id}/domains |
Create Domain in Pod |
GET |
/v0/pods/{pod_id}/domains/{domain_id} |
Get Domain in Pod |
PATCH |
/v0/pods/{pod_id}/domains/{domain_id} |
Update Domain in Pod |
DELETE |
/v0/pods/{pod_id}/domains/{domain_id} |
Delete Domain in Pod |
GET |
/v0/pods/{pod_id}/domains/{domain_id}/domain-connect |
Get Domain Connect Setup in Pod |
POST |
/v0/pods/{pod_id}/domains/{domain_id}/verify |
Verify Domain in Pod |
GET |
/v0/pods/{pod_id}/domains/{domain_id}/zone-file |
Get Zone File in Pod |
List Domains
GET /v0/domains
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: domain_read.
Query parameters
| Field | Type | Description | |
|---|---|---|---|
limit |
integer |
Maximum number of items to return. Default 50, maximum 500; larger values are capped. | |
page_token |
string |
next_page_token from the previous page. | |
ascending |
boolean |
Sort oldest first. |
Response
200 A page of domains (application/json)
| Field | Type | Description | |
|---|---|---|---|
count |
integer |
required | Number of items returned. |
limit |
integer |
Maximum number of items per page. Default 50, maximum 500; larger values are capped. | |
next_page_token |
string |
Page token for pagination. Absent on the last page. | |
domains |
DomainItem[] |
required | Ordered by created_at, newest first unless ascending is set. |
Each item of domains is a DomainItem:
| Field | Type | Description | |
|---|---|---|---|
pod_id |
string |
required | ID of the pod the domain belongs to. |
domain_id |
string |
required | ID of the domain. |
domain |
string |
required | The domain name (for example example.com), lowercased. |
feedback_enabled |
boolean |
required | Stored as given. MachineMail does not send bounce and complaint notifications to inboxes; use the message.bounced and message.complained events. |
inbound_enabled |
boolean |
required | Receive email for this domain. false makes it send-only: the inbound MX record is not required. |
subdomains_enabled |
boolean |
required | Always false: inboxes on subdomains are not supported. |
tracking_enabled |
boolean |
required | Always false: open tracking is not supported. |
client_id |
string |
Client ID given at creation (idempotency key). | |
updated_at |
string (date-time) |
required | Time at which the domain was last updated. |
created_at |
string (date-time) |
required | Time at which the domain was created. |
status |
"NOT_STARTED" | "PENDING" | "INVALID" | "FAILED" | "VERIFYING" | "VERIFIED" |
required | Verification status, as on Domain. |
reason |
string |
Why the domain is not verified yet, as on Domain. | |
records |
VerificationRecord[] |
required | DNS records to publish, as on Domain. |
organization_id |
string |
required | ID of the organization. |
catch_all |
boolean |
required | Mail to any address at the domain without an inbox creates one (created_by: catch_all). |
catch_all_ttl_seconds |
integer |
TTL of inboxes created by catch-all; absent means they are permanent. | |
nameservers |
string[] |
Authoritative nameservers, detected on create and verify. | |
dns_provider |
"cloudflare" | "route53" | "google" | "vercel" | "namecheap" | "godaddy" | "digitalocean" | "hetzner" | "porkbun" | "other" |
DNS host the nameservers belong to. | |
dns_provider_url |
string |
Link to the DNS host console. | |
domain_connect |
object |
One-click DNS setup (Domain Connect), from the last discovery. See Get Domain Connect Setup. | |
verified_at |
string (date-time) |
When the domain became VERIFIED. | |
last_checked_at |
string (date-time) |
When the DNS records were last checked. |
Errors
| Status | Meaning |
|---|---|
400 |
Invalid request: error.type is validation and error.param names the field. |
401 |
Missing, invalid, expired or revoked credential. |
403 |
The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account_suspended: error.message says what still works; do not retry or re-register). |
500 |
Unexpected server error. |
Error bodies use the error envelope.
Example
curl "https://api.machinemail.dev/v0/domains" \
-H "Authorization: Bearer $MACHINEMAIL_API_KEY"
{
"count": 1,
"limit": 50,
"next_page_token": "eyJvIjo1MH0",
"domains": [
{
"pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
"domain_id": "dom_01k6x3d5kq0000g3q9sx5b8j0m",
"domain": "example.com",
"feedback_enabled": false,
"inbound_enabled": false,
"subdomains_enabled": false,
"tracking_enabled": false,
"client_id": "my-client-id",
"updated_at": "2026-10-01T12:00:00.000Z",
"created_at": "2026-10-01T12:00:00.000Z",
"status": "NOT_STARTED",
"reason": "string",
"records": [
{
"type": "TXT",
"name": "Production",
"value": "string",
"status": "MISSING",
"priority": 1,
"reason": "string",
"purpose": "inbound",
"required": false
}
],
"organization_id": "org_01k6x2m9qd0000a7h3kq9v2c4e",
"catch_all": false,
"catch_all_ttl_seconds": 1,
"nameservers": ["string"],
"dns_provider": "cloudflare",
"dns_provider_url": "https://example.com",
"domain_connect": {
"supported": false,
"provider_name": "string",
"apply_url": "https://example.com"
},
"verified_at": "2026-10-01T12:00:00.000Z",
"last_checked_at": "2026-10-01T12:00:00.000Z"
}
]
}
Create Domain
POST /v0/domains
Registers the domain and returns the DNS records to publish (status: NOT_STARTED). Publish them, then call Verify Domain. A background worker re-checks PENDING, INVALID and VERIFYING domains about every 5 minutes until 7 days after creation; a NOT_STARTED domain is only checked by Verify Domain. A domain that is never verified is removed 14 days after it was added, together with its inboxes and the mail in them; owners and admins of the organization are emailed at least 3 days before. A domain that was verified once is never removed this way.
A domain can be registered by one organization only (409), and the shared domain cannot be added (409). Inbox-scoped credentials cannot add domains (403).
allow_conflicting_provider, subdomains_enabled and tracking_enabled are not supported.
502 upstream_error when the email provider cannot register the domain (for example its domain capacity is reached, or it is rate limiting registrations): nothing is created.
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: domain_create.
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
domain |
string |
required | The domain name, for example example.com. Cannot be the shared domain or a domain another organization registered. |
feedback_enabled |
boolean |
Stored and returned; has no effect. Default false. | |
inbound_enabled |
boolean |
Receive email for this domain. Default true. false makes it send-only (the inbound MX record is not required). | |
client_id |
string |
Idempotency key: a create with a client_id already used in the organization returns that domain with 200, or 409 when that domain is outside the pod of the API key or route. | |
pod_id |
string |
Pod to create the domain in. Defaults to the credential pod or the organization default pod. A pod-scoped credential can only name its own pod. | |
catch_all |
boolean |
Create an inbox for any address at the domain that receives mail. Default false. | |
catch_all_ttl_seconds |
integer | null |
Make inboxes created by catch-all ephemeral with this TTL. null or absent: permanent. |
Response
200 A domain with this client_id already exists; it is returned unchanged. (application/json)
Each item of records is a VerificationRecord:
| Field | Type | Description | |
|---|---|---|---|
type |
"TXT" | "CNAME" | "MX" |
required | |
name |
string |
required | Name (host) of the record. |
value |
string |
required | Value of the record. |
status |
"MISSING" | "INVALID" | "VALID" |
required | MISSING: not published yet. INVALID: published with the wrong value. |
priority |
integer |
Priority of an MX record. | |
reason |
string |
Why an INVALID record is invalid: Found: |
|
purpose |
"inbound" | "spf" | "dkim" | "dmarc" | "provider" |
required | What the record is for. provider records come from the sending provider. |
required |
boolean |
required | Whether the record must be valid for the domain to verify. The inbound MX record is not required when inbound_enabled is false. |
201 Created (application/json)
Each item of records is a VerificationRecord:
Errors
| Status | Meaning |
|---|---|
400 |
Invalid request: error.type is validation and error.param names the field. |
401 |
Missing, invalid, expired or revoked credential. |
402 |
Free plan: the organization already has its 1 custom domain (limit_key: "custom_domains"). Paid plans can add domains past their allowance (billed as extra). |
403 |
The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account_suspended: error.message says what still works; do not retry or re-register). |
409 |
Conflicts with an existing resource. |
500 |
Unexpected server error. |
502 |
The email provider refused or failed the send (error.type is upstream_error). Nothing was sent and no message was stored. error.message says why: a rejected message or recipient is final, and so is a used-up sending quota until it resets; a throttled or unavailable provider can be retried later. A scheduled draft whose send fails this way is marked send_status: failed, unless the provider could not take mail for now (throttled, sending paused, quota used up, or unavailable): it stays scheduled and is retried. |
Error bodies use the error envelope.
Example
curl -X POST "https://api.machinemail.dev/v0/domains" \
-H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"domain": "example.com",
"feedback_enabled": false
}'
{
"pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
"domain_id": "dom_01k6x3d5kq0000g3q9sx5b8j0m",
"domain": "example.com",
"feedback_enabled": false,
"inbound_enabled": false,
"subdomains_enabled": false,
"tracking_enabled": false,
"client_id": "my-client-id",
"updated_at": "2026-10-01T12:00:00.000Z",
"created_at": "2026-10-01T12:00:00.000Z",
"status": "NOT_STARTED",
"reason": "string",
"records": [
{
"type": "TXT",
"name": "Production",
"value": "string",
"status": "MISSING",
"priority": 1,
"reason": "string",
"purpose": "inbound",
"required": false
}
],
"organization_id": "org_01k6x2m9qd0000a7h3kq9v2c4e",
"catch_all": false,
"catch_all_ttl_seconds": 1,
"nameservers": ["string"],
"dns_provider": "cloudflare",
"dns_provider_url": "https://example.com",
"domain_connect": {
"supported": false,
"provider_name": "string",
"apply_url": "https://example.com"
},
"verified_at": "2026-10-01T12:00:00.000Z",
"last_checked_at": "2026-10-01T12:00:00.000Z"
}
Get Domain
GET /v0/domains/{domain_id}
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: domain_read.
Path parameters
| Field | Type | Description | |
|---|---|---|---|
domain_id |
string |
required | ID of the domain. |
Response
200 The domain (application/json)
Each item of records is a VerificationRecord:
Errors
| Status | Meaning |
|---|---|
401 |
Missing, invalid, expired or revoked credential. |
403 |
The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account_suspended: error.message says what still works; do not retry or re-register). |
404 |
Not found. Also returned for resources outside the credential scope. |
500 |
Unexpected server error. |
Error bodies use the error envelope.
Example
curl "https://api.machinemail.dev/v0/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m" \
-H "Authorization: Bearer $MACHINEMAIL_API_KEY"
Update Domain
PATCH /v0/domains/{domain_id}
Fields you omit are unchanged. subdomains_enabled and tracking_enabled are not supported.
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: domain_update.
Path parameters
| Field | Type | Description | |
|---|---|---|---|
domain_id |
string |
required | ID of the domain. |
Request body
Content type: application/json.
| Field | Type | Description | |
|---|---|---|---|
feedback_enabled |
boolean |
Stored and returned; has no effect. | |
inbound_enabled |
boolean |
Receive email for this domain. false makes the inbound MX record optional and re-evaluates the status. | |
catch_all |
boolean |
Create an inbox for any address at the domain that receives mail. | |
catch_all_ttl_seconds |
integer | null |
TTL of inboxes created by catch-all from now on. null makes them permanent. |
Response
200 The updated domain (application/json)
Each item of records is a VerificationRecord:
Errors
Error bodies use the error envelope.
Example
curl -X PATCH "https://api.machinemail.dev/v0/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m" \
-H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"feedback_enabled": false
}'
Delete Domain
DELETE /v0/domains/{domain_id}
Inboxes on the domain are not deleted.
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: domain_delete.
Path parameters
| Field | Type | Description | |
|---|---|---|---|
domain_id |
string |
required | ID of the domain. |
Response
200 Deleted (application/json)
| Field | Type | Description | |
|---|---|---|---|
ok |
true |
required |
Errors
Error bodies use the error envelope.
Example
curl -X DELETE "https://api.machinemail.dev/v0/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m" \
-H "Authorization: Bearer $MACHINEMAIL_API_KEY"
{
"ok": true
}
Get Domain Connect Setup
GET /v0/domains/{domain_id}/domain-connect
Discovers whether the domain's DNS provider supports Domain Connect (TXT _domainconnect.<zone> and the provider's /v2/<zone>/settings) and has onboarded the MachineMail template (machinemail.dev/email). When it has, apply_url is a signed synchronous-flow link (RS256, key=_dck1, sig last) that opens the provider with the outstanding MX, SPF, DKIM and DMARC records prefilled. Discovery is cached on the domain (6 h; provider errors 10 min) and re-run by Verify Domain.
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: domain_read.
Path parameters
| Field | Type | Description | |
|---|---|---|---|
domain_id |
string |
required | ID of the domain. |
Response
200 Domain Connect support for the domain (application/json)
| Field | Type | Description | |
|---|---|---|---|
supported |
boolean |
required | The zone DNS provider supports Domain Connect and has the MachineMail template. |
provider_name |
string |
Display name of the DNS provider. | |
apply_url |
string |
Signed link that opens the provider with the outstanding records prefilled. Absent when nothing is left to add. | |
provider_id |
string |
Domain Connect provider ID. | |
url_sync_ux |
string |
Base URL of the provider synchronous flow. | |
width |
integer |
Suggested popup width, in pixels. | |
height |
integer |
Suggested popup height, in pixels. | |
reason |
"no_domain_connect" | "provider_error" | "no_sync_flow" | "template_not_supported" | "records_not_templatable" | "already_configured" | "not_configured" |
Why one-click setup is not available, or already_configured when nothing is left to add. | |
checked_at |
string (date-time) |
required | When discovery last ran. |
Errors
Error bodies use the error envelope.
Example
curl "https://api.machinemail.dev/v0/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m/domain-connect" \
-H "Authorization: Bearer $MACHINEMAIL_API_KEY"
{
"supported": false,
"provider_name": "string",
"apply_url": "https://example.com",
"provider_id": "string",
"url_sync_ux": "string",
"width": 1,
"height": 1,
"reason": "no_domain_connect",
"checked_at": "2026-10-01T12:00:00.000Z"
}
Verify Domain
POST /v0/domains/{domain_id}/verify
Checks every DNS record now, updates records[].status and status, and re-runs Domain Connect discovery. A domain.verified event is emitted when the domain becomes VERIFIED.
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: domain_update.
Path parameters
| Field | Type | Description | |
|---|---|---|---|
domain_id |
string |
required | ID of the domain. |
Response
200 The domain after the check (application/json)
Each item of records is a VerificationRecord:
Errors
Error bodies use the error envelope.
Example
curl -X POST "https://api.machinemail.dev/v0/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m/verify" \
-H "Authorization: Bearer $MACHINEMAIL_API_KEY"
Get Zone File
GET /v0/domains/{domain_id}/zone-file
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: domain_read.
Path parameters
| Field | Type | Description | |
|---|---|---|---|
domain_id |
string |
required | ID of the domain. |
Response
200 A BIND zone file with the records to publish (text/plain)
Headers:
| Field | Type | Description | |
|---|---|---|---|
Content-Disposition |
string |
attachment; filename=" |
Errors
Error bodies use the error envelope.
Example
curl "https://api.machinemail.dev/v0/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m/zone-file" \
-H "Authorization: Bearer $MACHINEMAIL_API_KEY"
List Domains in Pod
GET /v0/pods/{pod_id}/domains
Same as List Domains (GET /v0/domains), limited to the pod.
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: domain_read.
Path parameters
| Field | Type | Description | |
|---|---|---|---|
pod_id |
string |
required | ID of the pod. |
Query parameters
Response
200 A page of domains (application/json)
Each item of domains is a DomainItem:
Errors
Error bodies use the error envelope.
Example
curl "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/domains" \
-H "Authorization: Bearer $MACHINEMAIL_API_KEY"
Create Domain in Pod
POST /v0/pods/{pod_id}/domains
Same as Create Domain (POST /v0/domains), limited to the pod.
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: domain_create.
Path parameters
| Field | Type | Description | |
|---|---|---|---|
pod_id |
string |
required | ID of the pod. |
Request body
Content type: application/json.
Response
200 A domain with this client_id already exists; it is returned unchanged. (application/json)
Each item of records is a VerificationRecord:
201 Created (application/json)
Each item of records is a VerificationRecord:
Errors
Error bodies use the error envelope.
Example
curl -X POST "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/domains" \
-H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"domain": "example.com",
"feedback_enabled": false
}'
Get Domain in Pod
GET /v0/pods/{pod_id}/domains/{domain_id}
Same as Get Domain (GET /v0/domains/{domain_id}), limited to the pod.
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: domain_read.
Path parameters
| Field | Type | Description | |
|---|---|---|---|
pod_id |
string |
required | ID of the pod. |
domain_id |
string |
required | ID of the domain. |
Response
200 The domain (application/json)
Each item of records is a VerificationRecord:
Errors
Error bodies use the error envelope.
Example
curl "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m" \
-H "Authorization: Bearer $MACHINEMAIL_API_KEY"
Update Domain in Pod
PATCH /v0/pods/{pod_id}/domains/{domain_id}
Same as Update Domain (PATCH /v0/domains/{domain_id}), limited to the pod.
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: domain_update.
Path parameters
Request body
Content type: application/json.
Response
200 The updated domain (application/json)
Each item of records is a VerificationRecord:
Errors
Error bodies use the error envelope.
Example
curl -X PATCH "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m" \
-H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"feedback_enabled": false
}'
Delete Domain in Pod
DELETE /v0/pods/{pod_id}/domains/{domain_id}
Same as Delete Domain (DELETE /v0/domains/{domain_id}), limited to the pod.
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: domain_delete.
Path parameters
Response
200 Deleted (application/json)
| Field | Type | Description | |
|---|---|---|---|
ok |
true |
required |
Errors
Error bodies use the error envelope.
Example
curl -X DELETE "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m" \
-H "Authorization: Bearer $MACHINEMAIL_API_KEY"
{
"ok": true
}
Get Domain Connect Setup in Pod
GET /v0/pods/{pod_id}/domains/{domain_id}/domain-connect
Same as Get Domain Connect Setup (GET /v0/domains/{domain_id}/domain-connect), limited to the pod.
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: domain_read.
Path parameters
Response
200 Domain Connect support for the domain (application/json)
Errors
Error bodies use the error envelope.
Example
curl "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m/domain-connect" \
-H "Authorization: Bearer $MACHINEMAIL_API_KEY"
Verify Domain in Pod
POST /v0/pods/{pod_id}/domains/{domain_id}/verify
Same as Verify Domain (POST /v0/domains/{domain_id}/verify), limited to the pod.
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: domain_update.
Path parameters
Response
200 The domain after the check (application/json)
Each item of records is a VerificationRecord:
Errors
Error bodies use the error envelope.
Example
curl -X POST "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m/verify" \
-H "Authorization: Bearer $MACHINEMAIL_API_KEY"
Get Zone File in Pod
GET /v0/pods/{pod_id}/domains/{domain_id}/zone-file
Same as Get Zone File (GET /v0/domains/{domain_id}/zone-file), limited to the pod.
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: domain_read.
Path parameters
Response
200 A BIND zone file with the records to publish (text/plain)
Headers:
| Field | Type | Description | |
|---|---|---|---|
Content-Disposition |
string |
attachment; filename=" |
Errors
Error bodies use the error envelope.
Example
curl "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m/zone-file" \
-H "Authorization: Bearer $MACHINEMAIL_API_KEY"