---
title: "Trusted domains"
description: "Mark the domains and addresses you work with as trusted, so their genuinely signed mail is never quarantined or withheld from your agents."
canonical: "https://machinemail.dev/docs/concepts/trusted-domains"
---

# Trusted domains

> Mark the domains and addresses you work with as trusted, so their genuinely signed mail is never quarantined or withheld from your agents.

Source: https://machinemail.dev/docs/concepts/trusted-domains

> **Note:** An entry changes how matching mail is judged from the next message that arrives. Adding an entry needs an organization API key or the dashboard, and an agent that has not been claimed into an
> account cannot have any: claim it first.

A trusted domain is a vendor, partner or service you rely on: mail it genuinely signs is labeled `TRUSTED`, is never marked `suspicious` or quarantined, and its text and attachments reach your agents in full. Trust changes how mail is judged, never who gets through:

- A **block entry** that matches the sender, at any level, wins over trust.
- An **allow-only** scope (one with `receive/allow` entries) still blocks every sender it does not allow. Adding a trusted domain never opens it.
- Trust applies only to mail that carries a **DKIM signature of the trusted domain itself**: the signature's `d=` must equal the domain (or be a subdomain of it, with `include_subdomains`). A relaxed DMARC pass, SPF alone or a forwarder's signature is not enough, so a spoofed `From:` is judged as usual.
- Malware is still withheld, and instructions aimed at agents are still flagged, whoever sends them.

Trusted domains are a MachineMail addition: AgentMail's API has no such resource.

## Add a trusted domain

```bash title="Terminal"
curl https://api.machinemail.dev/v0/trusted-domains \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "vendor.example", "reason": "Vendor invoices"}'
```

```json title="Response (201)"
{
  "trusted_domain_id": "td_01m5a2k8c00000a1b2c3d4e5f6",
  "entry_type": "domain",
  "domain": "vendor.example",
  "domain_unicode": "vendor.example",
  "include_subdomains": false,
  "scope": "org",
  "pod_id": null,
  "inbox_id": null,
  "reason": "Vendor invoices",
  "expires_at": null,
  "created_by": { "type": "api_key", "id": "key_01m4fy8h140000efvagszh6rpx" },
  "match_count": 0,
  "last_matched_at": null,
  "created_at": "2026-10-19T09:00:00.000Z",
  "updated_at": "2026-10-19T09:00:00.000Z",
  "warnings": []
}
```

| Field                | Notes                                                                                                  |
| -------------------- | ------------------------------------------------------------------------------------------------------ |
| `domain`             | A domain (`vendor.example`) or one exact address (`invoices@vendor.example`). Required.                |
| `include_subdomains` | Also trust mail signed by subdomains, such as `billing.vendor.example`. Default `false`. Domains only. |
| `reason`             | Why you trust it, for the people reading the list. Never included in a message's trust report.         |
| `expires_at`         | When the entry stops applying (useful for vendors you work with for a while). Must be in the future.   |
| `confirm`            | `true` to trust a platform where anyone can write the content (see below).                             |
| `client_id`          | Idempotency key: a second create with the same `client_id` returns the first entry with `200`.         |

Adding a domain that already exists at the same level returns the existing entry with `200`, unchanged. Internationalized names are stored in their ASCII (punycode) form; responses carry both forms, `domain` and `domain_unicode`.

## Levels

As with [lists](https://machinemail.dev/docs/concepts/lists-and-suppressions), the path decides where an entry lives, and mail to an inbox is judged against all three levels together:

| Level        | Path                                     |
| ------------ | ---------------------------------------- |
| Organization | `/v0/trusted-domains`                    |
| Pod          | `/v0/pods/{pod_id}/trusted-domains`      |
| Inbox        | `/v0/inboxes/{inbox_id}/trusted-domains` |

Each level supports:

| Method   | Path                                   | Notes                                                                       |
| -------- | -------------------------------------- | --------------------------------------------------------------------------- |
| `GET`    | `/trusted-domains`                     | Entries stored at this exact level, newest first. Paginated.                |
| `POST`   | `/trusted-domains`                     | Add one. `201`, or `200` for an existing entry.                             |
| `POST`   | `/trusted-domains/preview`             | What adding it would do, without saving anything.                           |
| `GET`    | `/trusted-domains/suggestions`         | Senders you regularly correspond with that you could trust.                 |
| `GET`    | `/trusted-domains/{trusted_domain_id}` | One entry.                                                                  |
| `PATCH`  | `/trusted-domains/{trusted_domain_id}` | Change `include_subdomains`, `reason` or `expires_at` (`null` clears them). |
| `DELETE` | `/trusted-domains/{trusted_domain_id}` | `204`. Mail from the domain is judged normally again.                       |

## Check an entry before adding it

`POST /trusted-domains/preview` answers what adding an entry would do, without saving anything:

- `allowed`, `refusal` and `confirmation_required`: whether it can be trusted, and whether it needs `confirm: true`.
- `warnings`: block entries and allow-only scopes that would still stop its mail.
- `would_release.count`: how many held messages `apply_to_existing` would release.
- `suggestions`: up to five exact addresses seen from that domain, most frequent first, when trusting one address is the safer choice.
- `evidence`: `dkim_seen` (we have verified the domain's own DKIM signature on mail to you in the last 90 days), `first_contact` (no mail from it before), `lookalike_of` (a well-known domain, or one you already trust, that it could be mistaken for) and `recent_auth_failures` (messages from it in the last 30 days that failed authentication).

Treat `first_contact` or a `lookalike_of` as a reason to stop and look. When `dkim_seen` is `false`, the entry is saved but changes nothing until the sender signs its mail.

`GET /trusted-domains/suggestions` lists senders you regularly correspond with whose mail is signed, that can be trusted and are not already trusted or blocked at this level. For a free mail provider it suggests the exact address, never the provider.

## Mail you already received

Adding an entry affects mail from the next message on. To also release mail that is already held, add `"apply_to_existing": true`: the response's `released.count` is the number of messages released.

- Only mail from the last 30 days that was held because of the sender or its authentication history, and that carries the trusted domain's own DKIM signature, is released.
- Mail held for malware, instructions aimed at agents or phishing is never released. Neither is mail a person moved to quarantine, or mail a block entry or allow-only scope stops.
- Each released message emits `message.released`, not `message.received`, so an agent is not handed a backlog as if it had just arrived.
- Mail that arrived before trusted domains became available on 11 November 2026 cannot be released this way. Release it by hand from the [quarantine view](https://machinemail.dev/docs/guides/review-quarantine).

Removing an entry, or turning `include_subdomains` off, puts the mail it vouched for back under its ordinary verdict: the level and the labels it would have had without trust. If another of your entries still applies to a message, it stays trusted under that one.

## Lookalikes

Once you trust a domain, mail that imitates it is judged more harshly: a sender whose domain could be mistaken for a trusted one, or whose display name shows a trusted domain while the address is elsewhere, is reported as impersonating it.

## What cannot be trusted

Some names are refused with `422 trusted_domain_not_allowed`, because anyone can get mail from them. `error.reason` says which:

| `reason`          | Example                     | Why                                                                    |
| ----------------- | --------------------------- | ---------------------------------------------------------------------- |
| `free_mail`       | `gmail.com`                 | Anyone can get an address there. Trust one address instead.            |
| `privacy_relay`   | `duck.com`                  | Every address forwards someone else's mail.                            |
| `shared_platform` | `machinemail.dev`           | Every MachineMail organization can send from it.                       |
| `public_suffix`   | `co.uk`, `github.io`        | Anyone can register a name under it.                                   |
| `disposable`      | `mailinator.com`            | A throwaway mail service.                                              |
| `invalid`         | `not a domain`              | Not a domain name or email address.                                    |
| `confusable_idn`  | `pаypal.com` (Cyrillic `а`) | An internationalized name that mixes scripts or imitates a Latin name. |

An exact address at a free mail provider, such as `alice@gmail.com`, can be trusted: the provider stops anyone else sending as that address.

### Platforms that need a confirmation

On platforms where anyone can write the content of a message, such as github.com, google.com or slack.com, trusting the domain trusts every user of the platform. Adding one returns `422 confirmation_required` (`error.reason: "user_content_platform"`) until you resend with `"confirm": true`. Usually the better choice is one address, such as `notifications@github.com`, which needs no confirmation.

The same applies when `include_subdomains` would cover such a platform: `amazon.de` with its subdomains includes the marketplace relay `marketplace.amazon.de`, so it needs `"confirm": true`, on create or when a `PATCH` turns `include_subdomains` on. One address on a group or marketplace relay, such as `team@googlegroups.com`, needs it too, because whoever posts to the group sends as that address. A domain whose subdomains include a privacy relay or a MachineMail shared domain (`appleid.com`, which has `privaterelay.appleid.com` under it) can be trusted only without its subdomains.

### Warnings

Conflicts never refuse an entry. When a block entry matches some of the mail an entry would trust, or a scope it applies to is allow-only, the entry is saved and the response lists `warnings`:

```json title="warnings"
[
  {
    "code": "shadowed_by_block",
    "scope": "inbox",
    "pod_id": "pod_01m4fy8h140000efvagszh6rpx",
    "inbox_id": "support@agents.example",
    "entry": { "entry": "billing.vendor.example", "entry_type": "domain" },
    "message": "The inbox block entry billing.vendor.example still blocks some or all of this mail: a block wins over trust."
  }
]
```

`POST`, `PATCH` and `POST .../preview` return warnings; `GET` does not. The block entry itself (`entry`) is included only for keys that can read lists (`list_entry_read`).

## Who can add trusted domains

A trusted domain hands mail to your agents in full, so adding or changing one takes an **organization API key** with the permission, or a person in the dashboard. Pod and inbox keys, connected apps and agent access tokens get `403` whatever their permissions, and there is no MCP tool for it: an agent that reads a malicious email cannot be talked into trusting the attacker's domain.

| Permission              | Allows                                             |
| ----------------------- | -------------------------------------------------- |
| `trusted_domain_read`   | List, get and preview.                             |
| `trusted_domain_create` | Add (organization keys and the dashboard only).    |
| `trusted_domain_update` | Change (organization keys and the dashboard only). |
| `trusted_domain_delete` | Remove. Any key that can see the entry.            |

## Limits

| Plan                     | Trusted domains |
| ------------------------ | --------------- |
| Unclaimed sandbox agents | None            |
| Free                     | 10              |
| Pro, Team and Scale      | 500             |

The limit counts entries at every level of the organization. Past it, adding one returns `402 plan_limit_exceeded` with `error.limit_key` `trusted_domains`, `error.limit` and `error.used`, like the other [plan limits](https://machinemail.dev/docs/reference/errors#plan-limits). Remove an entry, or upgrade, to add another.

## Events

Every change is recorded in the [event log](https://machinemail.dev/docs/concepts/events) as `trusted_domain.created`, `trusted_domain.updated` or `trusted_domain.deleted`, with the entry (and its `created_by`) under `data.trusted_domain`: together they are the audit trail. Webhooks and WebSocket subscriptions receive these events only when their `event_types` list them. The same goes for `message.released`, which carries the message and its thread like `message.received`.

## Next

- [Lists and suppressions](https://machinemail.dev/docs/concepts/lists-and-suppressions)
- [Trust layer](https://machinemail.dev/docs/trust)
