# Custom domains

> Send and receive from your own domain: DNS records, verification and limits.

Source: https://developer.mueslimail.com/docs/custom-domains/

Use your own domain (or a subdomain such as `agents.your-domain.eu`) so recipients see your brand. A subdomain is recommended: it keeps agent mail separate from your team's mail.

## 1. Add the domain

```bash
curl -s -X POST https://api.mueslimail.com/v0/domains \
  -H "Authorization: Bearer $MAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "agents.your-domain.eu"}'
```

The response contains the DNS records to publish. A fresh DKIM key is generated for each domain. Copy the values exactly as returned; the table shows their shape.

| Type | Name | Value | Purpose |
| --- | --- | --- | --- |
| MX | `agents.your-domain.eu` | `10 <platform mail host>.` | Routes inbound mail to the platform. |
| TXT | `agents.your-domain.eu` | `v=spf1 mx include:<relay> -all` | SPF: authorizes the platform to send. |
| TXT | `<selector>._domainkey.agents.your-domain.eu` | `v=DKIM1; k=rsa; p=<public key>` | DKIM: public key for signatures. |
| TXT | `_dmarc.agents.your-domain.eu` | `v=DMARC1; p=quarantine; rua=mailto:dmarc@agents.your-domain.eu` | DMARC policy. |

> Careful: Publishing an MX record moves inbound mail for that name to the platform. Do not add it to a domain whose mail you still receive elsewhere; use a subdomain instead.

## 2. Verify

Call `POST /v0/domains/{domain}/verify` to check DNS now. The scheduler also checks every 5 minutes for 7 days after you add the domain. Each record reports `verified` or `missing`; when all four are found the domain becomes `verified` and the `domain.verified` event fires.

## 3. Create inboxes on it

Pass `domain` when you [create an inbox](https://developer.mueslimail.com/docs/inboxes.md). Until the domain is verified the call returns `403 domain_not_verified`.

## Catch-all: one inbox for unknown addresses

A verified domain can nominate **one inbox** that receives the mail addressed to a local part nobody created: `PATCH /v0/domains/{domain}` with `{"catch_all_inbox": "triage@agents.your-domain.eu"}`, and `null` turns it off. The lookup is exact-address first, so an address that exists always wins and the catch-all only ever catches what would otherwise be refused.

* The nominated inbox must be on that same domain (`422 catch_all_inbox_wrong_domain`) and the domain must be verified (`422 domain_not_verified`); an unknown address is `404`.
* A **quarantined** address is never caught: an address whose inbox was deleted less than 90 days ago is refused, because the quarantine exists so that mail for the previous owner reaches nobody.
* One sender may fill at most `MAI_CATCH_ALL_PER_HOUR` unknown local parts per domain per hour (20 by default). Past that, the recipient is refused, not accepted: a catch-all without a budget is a backscatter amplifier.
* Caught mail is ordinary inbound mail: the same screening, the same rules, the same events. If the nominated inbox is deleted the catch-all switches itself off, and an unknown recipient is a `550` at RCPT, never a silent drop.

## Limits and deletion

* The number of custom domains depends on the plan: 1 on Free, 5 on Builder, 20 on Growth, 50 on Team.
* A domain can be deleted only when it has no inboxes left; otherwise the API returns `409 cannot_delete`.
* `GET /v0/domains` lists your domains and the shared platform domain.

## Endpoints

### POST /v0/domains

Add a custom domain; returns the DNS records to publish

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `domain` | string | required |

Responses:

* `201` Created: Domain
* `403` Error envelope: Error
* `409` Error envelope: Error

### GET /v0/domains

List own and shared domains

Responses:

* `200` OK

### GET /v0/domains/{domain}

Get a domain with record status

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `domain` | string | in path, required |

Responses:

* `200` OK: Domain

### DELETE /v0/domains/{domain}

Delete a domain without inboxes

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `domain` | string | in path, required |

Responses:

* `204` Deleted
* `409` Error envelope: Error

### POST /v0/domains/{domain}/verify

Check DNS now (the scheduler also checks every 5 minutes for 7 days)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `domain` | string | in path, required |

Responses:

* `200` OK: Domain

### Domain object

| Name | Type | Notes |
| --- | --- | --- |
| `domain_id` | string (uuid) |  |
| `domain` | string |  |
| `shared` | boolean |  |
| `status` | pending \| verified |  |
| `records` | object[] |  |
| `last_checked_at` | string (date-time) |  |
| `verified_at` | string (date-time) |  |
| `catch_all_inbox` | string (email) | The inbox that receives mail addressed to a local part nobody created on this domain, or null for no catch-all (D5) |
