---
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](#list-domains)                                       |
| `POST`   | `/v0/domains`                                          | [Create Domain](#create-domain)                                     |
| `GET`    | `/v0/domains/{domain_id}`                              | [Get Domain](#get-domain)                                           |
| `PATCH`  | `/v0/domains/{domain_id}`                              | [Update Domain](#update-domain)                                     |
| `DELETE` | `/v0/domains/{domain_id}`                              | [Delete Domain](#delete-domain)                                     |
| `GET`    | `/v0/domains/{domain_id}/domain-connect`               | [Get Domain Connect Setup](#get-domain-connect-setup)               |
| `POST`   | `/v0/domains/{domain_id}/verify`                       | [Verify Domain](#verify-domain)                                     |
| `GET`    | `/v0/domains/{domain_id}/zone-file`                    | [Get Zone File](#get-zone-file)                                     |
| `GET`    | `/v0/pods/{pod_id}/domains`                            | [List Domains in Pod](#list-domains-in-pod)                         |
| `POST`   | `/v0/pods/{pod_id}/domains`                            | [Create Domain in Pod](#create-domain-in-pod)                       |
| `GET`    | `/v0/pods/{pod_id}/domains/{domain_id}`                | [Get Domain in Pod](#get-domain-in-pod)                             |
| `PATCH`  | `/v0/pods/{pod_id}/domains/{domain_id}`                | [Update Domain in Pod](#update-domain-in-pod)                       |
| `DELETE` | `/v0/pods/{pod_id}/domains/{domain_id}`                | [Delete Domain in Pod](#delete-domain-in-pod)                       |
| `GET`    | `/v0/pods/{pod_id}/domains/{domain_id}/domain-connect` | [Get Domain Connect Setup in Pod](#get-domain-connect-setup-in-pod) |
| `POST`   | `/v0/pods/{pod_id}/domains/{domain_id}/verify`         | [Verify Domain in Pod](#verify-domain-in-pod)                       |
| `GET`    | `/v0/pods/{pod_id}/domains/{domain_id}/zone-file`      | [Get Zone File in Pod](#get-zone-file-in-pod)                       |

## List Domains

```http
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](https://machinemail.dev/docs/reference/api/objects#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. |
| `dns_conflicts` | `DnsConflict[]` |  | Existing DNS records that conflict with records, each with what to do about it. Looked up when the domain is added and again on every check. They never stop the domain from being added. Empty when there are none; absent when the lookup did not complete. |
| `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](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/domains" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "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",
      "dns_conflicts": [
        {
          "kind": "mx",
          "type": "TXT",
          "name": "Production",
          "found": ["string"],
          "provider": "string",
          "title": "string",
          "summary": "string",
          "why": "string",
          "message": "string",
          "options": [{}],
          "resolution": "string",
          "suggested_value": "string"
        }
      ],
      "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

```http
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`).

The domain's current DNS is looked up as it is added. Existing records that conflict with the ones to publish (MX records of another mail provider, an SPF record at the same name, a CNAME at a name that needs another record) are returned in `dns_conflicts`, each with what to do about it. Conflicts never refuse the domain, so `allow_conflicting_provider` is not supported; when the lookup does not complete, the domain is added without `dns_conflicts`.

`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`)

| 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 | NOT_STARTED until the first verify, then VERIFIED when every required record is valid, INVALID when a required record has the wrong value, otherwise PENDING. For domains whose email provider reports its own verdict (Amazon SES): VERIFYING while every required record checks out but the provider has not confirmed them yet, and FAILED when the provider gave up (reason says why). |
| `reason` | `string` |  | Why the domain is not verified yet: what to do next, or which records are missing or invalid. Absent once VERIFIED. |
| `records` | `VerificationRecord[]` | required | DNS records to publish. |
| `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. |
| `dns_conflicts` | `DnsConflict[]` |  | Existing DNS records that conflict with records, each with what to do about it. Looked up when the domain is added and again on every check. They never stop the domain from being added. Empty when there are none; absent when the lookup did not complete. |
| `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. |

Each item of `records` is a [VerificationRecord](https://machinemail.dev/docs/reference/api/objects#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: <values seen>. |
| `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. |

Each item of `dns_conflicts` is a [DnsConflict](https://machinemail.dev/docs/reference/api/objects#dnsconflict):

| Field | Type | | Description |
| --- | --- | --- | --- |
| `kind` | `"mx" \| "spf" \| "dmarc" \| "cname"` | required | mx: the name already has MX records for other mail servers. spf: the name already has an SPF record without the MachineMail include:, or has more than one SPF record. dmarc: the name has more than one DMARC record. cname: the name is a CNAME, so no other record can be published at it. |
| `type` | `"TXT" \| "CNAME" \| "MX"` | required | Type of the existing records. |
| `name` | `string` | required | Name (host) the existing records are at. |
| `found` | `string[]` | required | The existing records, as DNS returned them. MX records are <priority> <host>. |
| `provider` | `string` |  | mx only: the mail provider the existing MX records belong to (for example Google Workspace), when it is recognised. |
| `title` | `string` | required | One short line naming what conflicts, for example Existing MX records at Google Workspace. On a verified domain whose MX name also has another provider’s MX records it describes the current state: Mail for example.com is shared with Google Workspace. |
| `summary` | `string` | required | One short line with the consequence. |
| `why` | `string` | required | Why it matters, in a sentence or two. |
| `message` | `string` | required | What the existing records are and why they conflict, in one paragraph that ends with why. |
| `options` | `object[]` | required | The ways to resolve the conflict. One entry when there is a single thing to do; pick one when there are several. |
| `resolution` | `string` | required | What to do about it, in one paragraph: the label and detail of every option. |
| `suggested_value` | `string` |  | spf only: the existing SPF record with the MachineMail include: added. Publish it in place of the existing record. |

**201** Created (`application/json`)

| 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 | NOT_STARTED until the first verify, then VERIFIED when every required record is valid, INVALID when a required record has the wrong value, otherwise PENDING. For domains whose email provider reports its own verdict (Amazon SES): VERIFYING while every required record checks out but the provider has not confirmed them yet, and FAILED when the provider gave up (reason says why). |
| `reason` | `string` |  | Why the domain is not verified yet: what to do next, or which records are missing or invalid. Absent once VERIFIED. |
| `records` | `VerificationRecord[]` | required | DNS records to publish. |
| `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. |
| `dns_conflicts` | `DnsConflict[]` |  | Existing DNS records that conflict with records, each with what to do about it. Looked up when the domain is added and again on every check. They never stop the domain from being added. Empty when there are none; absent when the lookup did not complete. |
| `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. |

Each item of `records` is a [VerificationRecord](https://machinemail.dev/docs/reference/api/objects#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: <values seen>. |
| `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. |

Each item of `dns_conflicts` is a [DnsConflict](https://machinemail.dev/docs/reference/api/objects#dnsconflict):

| Field | Type | | Description |
| --- | --- | --- | --- |
| `kind` | `"mx" \| "spf" \| "dmarc" \| "cname"` | required | mx: the name already has MX records for other mail servers. spf: the name already has an SPF record without the MachineMail include:, or has more than one SPF record. dmarc: the name has more than one DMARC record. cname: the name is a CNAME, so no other record can be published at it. |
| `type` | `"TXT" \| "CNAME" \| "MX"` | required | Type of the existing records. |
| `name` | `string` | required | Name (host) the existing records are at. |
| `found` | `string[]` | required | The existing records, as DNS returned them. MX records are <priority> <host>. |
| `provider` | `string` |  | mx only: the mail provider the existing MX records belong to (for example Google Workspace), when it is recognised. |
| `title` | `string` | required | One short line naming what conflicts, for example Existing MX records at Google Workspace. On a verified domain whose MX name also has another provider’s MX records it describes the current state: Mail for example.com is shared with Google Workspace. |
| `summary` | `string` | required | One short line with the consequence. |
| `why` | `string` | required | Why it matters, in a sentence or two. |
| `message` | `string` | required | What the existing records are and why they conflict, in one paragraph that ends with why. |
| `options` | `object[]` | required | The ways to resolve the conflict. One entry when there is a single thing to do; pick one when there are several. |
| `resolution` | `string` | required | What to do about it, in one paragraph: the label and detail of every option. |
| `suggested_value` | `string` |  | spf only: the existing SPF record with the MachineMail include: added. Publish it in place of the existing record. |

#### 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](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
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
}'
```

```json title="Response 200"
{
  "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",
  "dns_conflicts": [
    {
      "kind": "mx",
      "type": "TXT",
      "name": "Production",
      "found": ["string"],
      "provider": "string",
      "title": "string",
      "summary": "string",
      "why": "string",
      "message": "string",
      "options": [
        {
          "label": "string",
          "detail": "string"
        }
      ],
      "resolution": "string",
      "suggested_value": "string"
    }
  ],
  "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

```http
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`)

| 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 | NOT_STARTED until the first verify, then VERIFIED when every required record is valid, INVALID when a required record has the wrong value, otherwise PENDING. For domains whose email provider reports its own verdict (Amazon SES): VERIFYING while every required record checks out but the provider has not confirmed them yet, and FAILED when the provider gave up (reason says why). |
| `reason` | `string` |  | Why the domain is not verified yet: what to do next, or which records are missing or invalid. Absent once VERIFIED. |
| `records` | `VerificationRecord[]` | required | DNS records to publish. |
| `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. |
| `dns_conflicts` | `DnsConflict[]` |  | Existing DNS records that conflict with records, each with what to do about it. Looked up when the domain is added and again on every check. They never stop the domain from being added. Empty when there are none; absent when the lookup did not complete. |
| `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. |

Each item of `records` is a [VerificationRecord](https://machinemail.dev/docs/reference/api/objects#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: <values seen>. |
| `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. |

Each item of `dns_conflicts` is a [DnsConflict](https://machinemail.dev/docs/reference/api/objects#dnsconflict):

| Field | Type | | Description |
| --- | --- | --- | --- |
| `kind` | `"mx" \| "spf" \| "dmarc" \| "cname"` | required | mx: the name already has MX records for other mail servers. spf: the name already has an SPF record without the MachineMail include:, or has more than one SPF record. dmarc: the name has more than one DMARC record. cname: the name is a CNAME, so no other record can be published at it. |
| `type` | `"TXT" \| "CNAME" \| "MX"` | required | Type of the existing records. |
| `name` | `string` | required | Name (host) the existing records are at. |
| `found` | `string[]` | required | The existing records, as DNS returned them. MX records are <priority> <host>. |
| `provider` | `string` |  | mx only: the mail provider the existing MX records belong to (for example Google Workspace), when it is recognised. |
| `title` | `string` | required | One short line naming what conflicts, for example Existing MX records at Google Workspace. On a verified domain whose MX name also has another provider’s MX records it describes the current state: Mail for example.com is shared with Google Workspace. |
| `summary` | `string` | required | One short line with the consequence. |
| `why` | `string` | required | Why it matters, in a sentence or two. |
| `message` | `string` | required | What the existing records are and why they conflict, in one paragraph that ends with why. |
| `options` | `object[]` | required | The ways to resolve the conflict. One entry when there is a single thing to do; pick one when there are several. |
| `resolution` | `string` | required | What to do about it, in one paragraph: the label and detail of every option. |
| `suggested_value` | `string` |  | spf only: the existing SPF record with the MachineMail include: added. Publish it in place of the existing record. |

#### 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](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "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",
  "dns_conflicts": [
    {
      "kind": "mx",
      "type": "TXT",
      "name": "Production",
      "found": ["string"],
      "provider": "string",
      "title": "string",
      "summary": "string",
      "why": "string",
      "message": "string",
      "options": [
        {
          "label": "string",
          "detail": "string"
        }
      ],
      "resolution": "string",
      "suggested_value": "string"
    }
  ],
  "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"
}
```

## Update Domain

```http
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`)

| 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 | NOT_STARTED until the first verify, then VERIFIED when every required record is valid, INVALID when a required record has the wrong value, otherwise PENDING. For domains whose email provider reports its own verdict (Amazon SES): VERIFYING while every required record checks out but the provider has not confirmed them yet, and FAILED when the provider gave up (reason says why). |
| `reason` | `string` |  | Why the domain is not verified yet: what to do next, or which records are missing or invalid. Absent once VERIFIED. |
| `records` | `VerificationRecord[]` | required | DNS records to publish. |
| `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. |
| `dns_conflicts` | `DnsConflict[]` |  | Existing DNS records that conflict with records, each with what to do about it. Looked up when the domain is added and again on every check. They never stop the domain from being added. Empty when there are none; absent when the lookup did not complete. |
| `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. |

Each item of `records` is a [VerificationRecord](https://machinemail.dev/docs/reference/api/objects#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: <values seen>. |
| `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. |

Each item of `dns_conflicts` is a [DnsConflict](https://machinemail.dev/docs/reference/api/objects#dnsconflict):

| Field | Type | | Description |
| --- | --- | --- | --- |
| `kind` | `"mx" \| "spf" \| "dmarc" \| "cname"` | required | mx: the name already has MX records for other mail servers. spf: the name already has an SPF record without the MachineMail include:, or has more than one SPF record. dmarc: the name has more than one DMARC record. cname: the name is a CNAME, so no other record can be published at it. |
| `type` | `"TXT" \| "CNAME" \| "MX"` | required | Type of the existing records. |
| `name` | `string` | required | Name (host) the existing records are at. |
| `found` | `string[]` | required | The existing records, as DNS returned them. MX records are <priority> <host>. |
| `provider` | `string` |  | mx only: the mail provider the existing MX records belong to (for example Google Workspace), when it is recognised. |
| `title` | `string` | required | One short line naming what conflicts, for example Existing MX records at Google Workspace. On a verified domain whose MX name also has another provider’s MX records it describes the current state: Mail for example.com is shared with Google Workspace. |
| `summary` | `string` | required | One short line with the consequence. |
| `why` | `string` | required | Why it matters, in a sentence or two. |
| `message` | `string` | required | What the existing records are and why they conflict, in one paragraph that ends with why. |
| `options` | `object[]` | required | The ways to resolve the conflict. One entry when there is a single thing to do; pick one when there are several. |
| `resolution` | `string` | required | What to do about it, in one paragraph: the label and detail of every option. |
| `suggested_value` | `string` |  | spf only: the existing SPF record with the MachineMail include: added. Publish it in place of the existing record. |

#### 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). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
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
}'
```

```json title="Response 200"
{
  "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",
  "dns_conflicts": [
    {
      "kind": "mx",
      "type": "TXT",
      "name": "Production",
      "found": ["string"],
      "provider": "string",
      "title": "string",
      "summary": "string",
      "why": "string",
      "message": "string",
      "options": [
        {
          "label": "string",
          "detail": "string"
        }
      ],
      "resolution": "string",
      "suggested_value": "string"
    }
  ],
  "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"
}
```

## Delete Domain

```http
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

| 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](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X DELETE "https://api.machinemail.dev/v0/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "ok": true
}
```

## Get Domain Connect Setup

```http
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

| 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](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m/domain-connect" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "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

```http
POST /v0/domains/{domain_id}/verify
```

Checks every DNS record now, updates `records[].status`, `status` and `dns_conflicts`, 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`)

| 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 | NOT_STARTED until the first verify, then VERIFIED when every required record is valid, INVALID when a required record has the wrong value, otherwise PENDING. For domains whose email provider reports its own verdict (Amazon SES): VERIFYING while every required record checks out but the provider has not confirmed them yet, and FAILED when the provider gave up (reason says why). |
| `reason` | `string` |  | Why the domain is not verified yet: what to do next, or which records are missing or invalid. Absent once VERIFIED. |
| `records` | `VerificationRecord[]` | required | DNS records to publish. |
| `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. |
| `dns_conflicts` | `DnsConflict[]` |  | Existing DNS records that conflict with records, each with what to do about it. Looked up when the domain is added and again on every check. They never stop the domain from being added. Empty when there are none; absent when the lookup did not complete. |
| `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. |

Each item of `records` is a [VerificationRecord](https://machinemail.dev/docs/reference/api/objects#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: <values seen>. |
| `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. |

Each item of `dns_conflicts` is a [DnsConflict](https://machinemail.dev/docs/reference/api/objects#dnsconflict):

| Field | Type | | Description |
| --- | --- | --- | --- |
| `kind` | `"mx" \| "spf" \| "dmarc" \| "cname"` | required | mx: the name already has MX records for other mail servers. spf: the name already has an SPF record without the MachineMail include:, or has more than one SPF record. dmarc: the name has more than one DMARC record. cname: the name is a CNAME, so no other record can be published at it. |
| `type` | `"TXT" \| "CNAME" \| "MX"` | required | Type of the existing records. |
| `name` | `string` | required | Name (host) the existing records are at. |
| `found` | `string[]` | required | The existing records, as DNS returned them. MX records are <priority> <host>. |
| `provider` | `string` |  | mx only: the mail provider the existing MX records belong to (for example Google Workspace), when it is recognised. |
| `title` | `string` | required | One short line naming what conflicts, for example Existing MX records at Google Workspace. On a verified domain whose MX name also has another provider’s MX records it describes the current state: Mail for example.com is shared with Google Workspace. |
| `summary` | `string` | required | One short line with the consequence. |
| `why` | `string` | required | Why it matters, in a sentence or two. |
| `message` | `string` | required | What the existing records are and why they conflict, in one paragraph that ends with why. |
| `options` | `object[]` | required | The ways to resolve the conflict. One entry when there is a single thing to do; pick one when there are several. |
| `resolution` | `string` | required | What to do about it, in one paragraph: the label and detail of every option. |
| `suggested_value` | `string` |  | spf only: the existing SPF record with the MachineMail include: added. Publish it in place of the existing record. |

#### 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](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X POST "https://api.machinemail.dev/v0/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m/verify" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "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",
  "dns_conflicts": [
    {
      "kind": "mx",
      "type": "TXT",
      "name": "Production",
      "found": ["string"],
      "provider": "string",
      "title": "string",
      "summary": "string",
      "why": "string",
      "message": "string",
      "options": [
        {
          "label": "string",
          "detail": "string"
        }
      ],
      "resolution": "string",
      "suggested_value": "string"
    }
  ],
  "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 Zone File

```http
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="<domain>.zone" |

#### 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](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m/zone-file" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

## List Domains in Pod

```http
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

| 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](https://machinemail.dev/docs/reference/api/objects#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. |
| `dns_conflicts` | `DnsConflict[]` |  | Existing DNS records that conflict with records, each with what to do about it. Looked up when the domain is added and again on every check. They never stop the domain from being added. Empty when there are none; absent when the lookup did not complete. |
| `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). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/domains" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "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",
      "dns_conflicts": [
        {
          "kind": "mx",
          "type": "TXT",
          "name": "Production",
          "found": ["string"],
          "provider": "string",
          "title": "string",
          "summary": "string",
          "why": "string",
          "message": "string",
          "options": [{}],
          "resolution": "string",
          "suggested_value": "string"
        }
      ],
      "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 in Pod

```http
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`.

| 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`)

| 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 | NOT_STARTED until the first verify, then VERIFIED when every required record is valid, INVALID when a required record has the wrong value, otherwise PENDING. For domains whose email provider reports its own verdict (Amazon SES): VERIFYING while every required record checks out but the provider has not confirmed them yet, and FAILED when the provider gave up (reason says why). |
| `reason` | `string` |  | Why the domain is not verified yet: what to do next, or which records are missing or invalid. Absent once VERIFIED. |
| `records` | `VerificationRecord[]` | required | DNS records to publish. |
| `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. |
| `dns_conflicts` | `DnsConflict[]` |  | Existing DNS records that conflict with records, each with what to do about it. Looked up when the domain is added and again on every check. They never stop the domain from being added. Empty when there are none; absent when the lookup did not complete. |
| `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. |

Each item of `records` is a [VerificationRecord](https://machinemail.dev/docs/reference/api/objects#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: <values seen>. |
| `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. |

Each item of `dns_conflicts` is a [DnsConflict](https://machinemail.dev/docs/reference/api/objects#dnsconflict):

| Field | Type | | Description |
| --- | --- | --- | --- |
| `kind` | `"mx" \| "spf" \| "dmarc" \| "cname"` | required | mx: the name already has MX records for other mail servers. spf: the name already has an SPF record without the MachineMail include:, or has more than one SPF record. dmarc: the name has more than one DMARC record. cname: the name is a CNAME, so no other record can be published at it. |
| `type` | `"TXT" \| "CNAME" \| "MX"` | required | Type of the existing records. |
| `name` | `string` | required | Name (host) the existing records are at. |
| `found` | `string[]` | required | The existing records, as DNS returned them. MX records are <priority> <host>. |
| `provider` | `string` |  | mx only: the mail provider the existing MX records belong to (for example Google Workspace), when it is recognised. |
| `title` | `string` | required | One short line naming what conflicts, for example Existing MX records at Google Workspace. On a verified domain whose MX name also has another provider’s MX records it describes the current state: Mail for example.com is shared with Google Workspace. |
| `summary` | `string` | required | One short line with the consequence. |
| `why` | `string` | required | Why it matters, in a sentence or two. |
| `message` | `string` | required | What the existing records are and why they conflict, in one paragraph that ends with why. |
| `options` | `object[]` | required | The ways to resolve the conflict. One entry when there is a single thing to do; pick one when there are several. |
| `resolution` | `string` | required | What to do about it, in one paragraph: the label and detail of every option. |
| `suggested_value` | `string` |  | spf only: the existing SPF record with the MachineMail include: added. Publish it in place of the existing record. |

**201** Created (`application/json`)

| 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 | NOT_STARTED until the first verify, then VERIFIED when every required record is valid, INVALID when a required record has the wrong value, otherwise PENDING. For domains whose email provider reports its own verdict (Amazon SES): VERIFYING while every required record checks out but the provider has not confirmed them yet, and FAILED when the provider gave up (reason says why). |
| `reason` | `string` |  | Why the domain is not verified yet: what to do next, or which records are missing or invalid. Absent once VERIFIED. |
| `records` | `VerificationRecord[]` | required | DNS records to publish. |
| `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. |
| `dns_conflicts` | `DnsConflict[]` |  | Existing DNS records that conflict with records, each with what to do about it. Looked up when the domain is added and again on every check. They never stop the domain from being added. Empty when there are none; absent when the lookup did not complete. |
| `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. |

Each item of `records` is a [VerificationRecord](https://machinemail.dev/docs/reference/api/objects#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: <values seen>. |
| `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. |

Each item of `dns_conflicts` is a [DnsConflict](https://machinemail.dev/docs/reference/api/objects#dnsconflict):

| Field | Type | | Description |
| --- | --- | --- | --- |
| `kind` | `"mx" \| "spf" \| "dmarc" \| "cname"` | required | mx: the name already has MX records for other mail servers. spf: the name already has an SPF record without the MachineMail include:, or has more than one SPF record. dmarc: the name has more than one DMARC record. cname: the name is a CNAME, so no other record can be published at it. |
| `type` | `"TXT" \| "CNAME" \| "MX"` | required | Type of the existing records. |
| `name` | `string` | required | Name (host) the existing records are at. |
| `found` | `string[]` | required | The existing records, as DNS returned them. MX records are <priority> <host>. |
| `provider` | `string` |  | mx only: the mail provider the existing MX records belong to (for example Google Workspace), when it is recognised. |
| `title` | `string` | required | One short line naming what conflicts, for example Existing MX records at Google Workspace. On a verified domain whose MX name also has another provider’s MX records it describes the current state: Mail for example.com is shared with Google Workspace. |
| `summary` | `string` | required | One short line with the consequence. |
| `why` | `string` | required | Why it matters, in a sentence or two. |
| `message` | `string` | required | What the existing records are and why they conflict, in one paragraph that ends with why. |
| `options` | `object[]` | required | The ways to resolve the conflict. One entry when there is a single thing to do; pick one when there are several. |
| `resolution` | `string` | required | What to do about it, in one paragraph: the label and detail of every option. |
| `suggested_value` | `string` |  | spf only: the existing SPF record with the MachineMail include: added. Publish it in place of the existing record. |

#### 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).                                                                                                                                                                                                                                                                                               |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `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](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
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
}'
```

```json title="Response 200"
{
  "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",
  "dns_conflicts": [
    {
      "kind": "mx",
      "type": "TXT",
      "name": "Production",
      "found": ["string"],
      "provider": "string",
      "title": "string",
      "summary": "string",
      "why": "string",
      "message": "string",
      "options": [
        {
          "label": "string",
          "detail": "string"
        }
      ],
      "resolution": "string",
      "suggested_value": "string"
    }
  ],
  "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 in Pod

```http
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`)

| 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 | NOT_STARTED until the first verify, then VERIFIED when every required record is valid, INVALID when a required record has the wrong value, otherwise PENDING. For domains whose email provider reports its own verdict (Amazon SES): VERIFYING while every required record checks out but the provider has not confirmed them yet, and FAILED when the provider gave up (reason says why). |
| `reason` | `string` |  | Why the domain is not verified yet: what to do next, or which records are missing or invalid. Absent once VERIFIED. |
| `records` | `VerificationRecord[]` | required | DNS records to publish. |
| `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. |
| `dns_conflicts` | `DnsConflict[]` |  | Existing DNS records that conflict with records, each with what to do about it. Looked up when the domain is added and again on every check. They never stop the domain from being added. Empty when there are none; absent when the lookup did not complete. |
| `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. |

Each item of `records` is a [VerificationRecord](https://machinemail.dev/docs/reference/api/objects#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: <values seen>. |
| `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. |

Each item of `dns_conflicts` is a [DnsConflict](https://machinemail.dev/docs/reference/api/objects#dnsconflict):

| Field | Type | | Description |
| --- | --- | --- | --- |
| `kind` | `"mx" \| "spf" \| "dmarc" \| "cname"` | required | mx: the name already has MX records for other mail servers. spf: the name already has an SPF record without the MachineMail include:, or has more than one SPF record. dmarc: the name has more than one DMARC record. cname: the name is a CNAME, so no other record can be published at it. |
| `type` | `"TXT" \| "CNAME" \| "MX"` | required | Type of the existing records. |
| `name` | `string` | required | Name (host) the existing records are at. |
| `found` | `string[]` | required | The existing records, as DNS returned them. MX records are <priority> <host>. |
| `provider` | `string` |  | mx only: the mail provider the existing MX records belong to (for example Google Workspace), when it is recognised. |
| `title` | `string` | required | One short line naming what conflicts, for example Existing MX records at Google Workspace. On a verified domain whose MX name also has another provider’s MX records it describes the current state: Mail for example.com is shared with Google Workspace. |
| `summary` | `string` | required | One short line with the consequence. |
| `why` | `string` | required | Why it matters, in a sentence or two. |
| `message` | `string` | required | What the existing records are and why they conflict, in one paragraph that ends with why. |
| `options` | `object[]` | required | The ways to resolve the conflict. One entry when there is a single thing to do; pick one when there are several. |
| `resolution` | `string` | required | What to do about it, in one paragraph: the label and detail of every option. |
| `suggested_value` | `string` |  | spf only: the existing SPF record with the MachineMail include: added. Publish it in place of the existing record. |

#### 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](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "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",
  "dns_conflicts": [
    {
      "kind": "mx",
      "type": "TXT",
      "name": "Production",
      "found": ["string"],
      "provider": "string",
      "title": "string",
      "summary": "string",
      "why": "string",
      "message": "string",
      "options": [
        {
          "label": "string",
          "detail": "string"
        }
      ],
      "resolution": "string",
      "suggested_value": "string"
    }
  ],
  "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"
}
```

## Update Domain in Pod

```http
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

| Field | Type | | Description |
| --- | --- | --- | --- |
| `pod_id` | `string` | required | ID of the pod. |
| `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`)

| 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 | NOT_STARTED until the first verify, then VERIFIED when every required record is valid, INVALID when a required record has the wrong value, otherwise PENDING. For domains whose email provider reports its own verdict (Amazon SES): VERIFYING while every required record checks out but the provider has not confirmed them yet, and FAILED when the provider gave up (reason says why). |
| `reason` | `string` |  | Why the domain is not verified yet: what to do next, or which records are missing or invalid. Absent once VERIFIED. |
| `records` | `VerificationRecord[]` | required | DNS records to publish. |
| `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. |
| `dns_conflicts` | `DnsConflict[]` |  | Existing DNS records that conflict with records, each with what to do about it. Looked up when the domain is added and again on every check. They never stop the domain from being added. Empty when there are none; absent when the lookup did not complete. |
| `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. |

Each item of `records` is a [VerificationRecord](https://machinemail.dev/docs/reference/api/objects#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: <values seen>. |
| `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. |

Each item of `dns_conflicts` is a [DnsConflict](https://machinemail.dev/docs/reference/api/objects#dnsconflict):

| Field | Type | | Description |
| --- | --- | --- | --- |
| `kind` | `"mx" \| "spf" \| "dmarc" \| "cname"` | required | mx: the name already has MX records for other mail servers. spf: the name already has an SPF record without the MachineMail include:, or has more than one SPF record. dmarc: the name has more than one DMARC record. cname: the name is a CNAME, so no other record can be published at it. |
| `type` | `"TXT" \| "CNAME" \| "MX"` | required | Type of the existing records. |
| `name` | `string` | required | Name (host) the existing records are at. |
| `found` | `string[]` | required | The existing records, as DNS returned them. MX records are <priority> <host>. |
| `provider` | `string` |  | mx only: the mail provider the existing MX records belong to (for example Google Workspace), when it is recognised. |
| `title` | `string` | required | One short line naming what conflicts, for example Existing MX records at Google Workspace. On a verified domain whose MX name also has another provider’s MX records it describes the current state: Mail for example.com is shared with Google Workspace. |
| `summary` | `string` | required | One short line with the consequence. |
| `why` | `string` | required | Why it matters, in a sentence or two. |
| `message` | `string` | required | What the existing records are and why they conflict, in one paragraph that ends with why. |
| `options` | `object[]` | required | The ways to resolve the conflict. One entry when there is a single thing to do; pick one when there are several. |
| `resolution` | `string` | required | What to do about it, in one paragraph: the label and detail of every option. |
| `suggested_value` | `string` |  | spf only: the existing SPF record with the MachineMail include: added. Publish it in place of the existing record. |

#### 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). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
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
}'
```

```json title="Response 200"
{
  "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",
  "dns_conflicts": [
    {
      "kind": "mx",
      "type": "TXT",
      "name": "Production",
      "found": ["string"],
      "provider": "string",
      "title": "string",
      "summary": "string",
      "why": "string",
      "message": "string",
      "options": [
        {
          "label": "string",
          "detail": "string"
        }
      ],
      "resolution": "string",
      "suggested_value": "string"
    }
  ],
  "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"
}
```

## Delete Domain in Pod

```http
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

| Field | Type | | Description |
| --- | --- | --- | --- |
| `pod_id` | `string` | required | ID of the pod. |
| `domain_id` | `string` | required | ID of the domain. |

#### Response

**200** Deleted (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `ok` | `true` | required |  |

#### 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](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X DELETE "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "ok": true
}
```

## Get Domain Connect Setup in Pod

```http
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

| Field | Type | | Description |
| --- | --- | --- | --- |
| `pod_id` | `string` | required | ID of the pod. |
| `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

| 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](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m/domain-connect" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "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 in Pod

```http
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

| Field | Type | | Description |
| --- | --- | --- | --- |
| `pod_id` | `string` | required | ID of the pod. |
| `domain_id` | `string` | required | ID of the domain. |

#### Response

**200** The domain after the check (`application/json`)

| 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 | NOT_STARTED until the first verify, then VERIFIED when every required record is valid, INVALID when a required record has the wrong value, otherwise PENDING. For domains whose email provider reports its own verdict (Amazon SES): VERIFYING while every required record checks out but the provider has not confirmed them yet, and FAILED when the provider gave up (reason says why). |
| `reason` | `string` |  | Why the domain is not verified yet: what to do next, or which records are missing or invalid. Absent once VERIFIED. |
| `records` | `VerificationRecord[]` | required | DNS records to publish. |
| `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. |
| `dns_conflicts` | `DnsConflict[]` |  | Existing DNS records that conflict with records, each with what to do about it. Looked up when the domain is added and again on every check. They never stop the domain from being added. Empty when there are none; absent when the lookup did not complete. |
| `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. |

Each item of `records` is a [VerificationRecord](https://machinemail.dev/docs/reference/api/objects#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: <values seen>. |
| `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. |

Each item of `dns_conflicts` is a [DnsConflict](https://machinemail.dev/docs/reference/api/objects#dnsconflict):

| Field | Type | | Description |
| --- | --- | --- | --- |
| `kind` | `"mx" \| "spf" \| "dmarc" \| "cname"` | required | mx: the name already has MX records for other mail servers. spf: the name already has an SPF record without the MachineMail include:, or has more than one SPF record. dmarc: the name has more than one DMARC record. cname: the name is a CNAME, so no other record can be published at it. |
| `type` | `"TXT" \| "CNAME" \| "MX"` | required | Type of the existing records. |
| `name` | `string` | required | Name (host) the existing records are at. |
| `found` | `string[]` | required | The existing records, as DNS returned them. MX records are <priority> <host>. |
| `provider` | `string` |  | mx only: the mail provider the existing MX records belong to (for example Google Workspace), when it is recognised. |
| `title` | `string` | required | One short line naming what conflicts, for example Existing MX records at Google Workspace. On a verified domain whose MX name also has another provider’s MX records it describes the current state: Mail for example.com is shared with Google Workspace. |
| `summary` | `string` | required | One short line with the consequence. |
| `why` | `string` | required | Why it matters, in a sentence or two. |
| `message` | `string` | required | What the existing records are and why they conflict, in one paragraph that ends with why. |
| `options` | `object[]` | required | The ways to resolve the conflict. One entry when there is a single thing to do; pick one when there are several. |
| `resolution` | `string` | required | What to do about it, in one paragraph: the label and detail of every option. |
| `suggested_value` | `string` |  | spf only: the existing SPF record with the MachineMail include: added. Publish it in place of the existing record. |

#### 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](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X POST "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m/verify" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "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",
  "dns_conflicts": [
    {
      "kind": "mx",
      "type": "TXT",
      "name": "Production",
      "found": ["string"],
      "provider": "string",
      "title": "string",
      "summary": "string",
      "why": "string",
      "message": "string",
      "options": [
        {
          "label": "string",
          "detail": "string"
        }
      ],
      "resolution": "string",
      "suggested_value": "string"
    }
  ],
  "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 Zone File in Pod

```http
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

| Field | Type | | Description |
| --- | --- | --- | --- |
| `pod_id` | `string` | required | ID of the pod. |
| `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="<domain>.zone" |

#### 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](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/domains/dom_01k6x3d5kq0000g3q9sx5b8j0m/zone-file" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```
