Skip to contentSkip to content
machinemail

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.

Note

Trusted domains are in preview and turned on per organization. Until they are on for yours, the /v0/trusted-domains endpoints answer 404. Email support@machinemail.dev to join the preview. During the preview you can manage entries, and the effect on incoming mail is being rolled out separately: until it reaches your organization, entries are stored and validated but do not change how mail is judged.

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

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"}'
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": []
}
FieldNotes
domainA domain (vendor.example) or one exact address (invoices@vendor.example). Required.
include_subdomainsAlso trust mail signed by subdomains, such as billing.vendor.example. Default false. Domains only.
reasonWhy you trust it, for the people reading the list. Never included in a message’s trust report.
expires_atWhen the entry stops applying (useful for vendors you work with for a while). Must be in the future.
confirmtrue to trust a platform where anyone can write the content (see below).
client_idIdempotency 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, the path decides where an entry lives, and mail to an inbox is judged against all three levels together:

LevelPath
Organization/v0/trusted-domains
Pod/v0/pods/{pod_id}/trusted-domains
Inbox/v0/inboxes/{inbox_id}/trusted-domains

Each level supports:

MethodPathNotes
GET/trusted-domainsEntries stored at this exact level, newest first. Paginated.
POST/trusted-domainsAdd one. 201, or 200 for an existing entry.
POST/trusted-domains/previewWhat adding it would do, without saving anything.
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.

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:

reasonExampleWhy
free_mailgmail.comAnyone can get an address there. Trust one address instead.
privacy_relayduck.comEvery address forwards someone else’s mail.
shared_platformmachinemail.devEvery MachineMail organization can send from it.
public_suffixco.uk, github.ioAnyone can register a name under it.
disposablemailinator.comA throwaway mail service.
invalidnot a domainNot a domain name or email address.
confusable_idnpа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:

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.

PermissionAllows
trusted_domain_readList, get and preview.
trusted_domain_createAdd (organization keys and the dashboard only).
trusted_domain_updateChange (organization keys and the dashboard only).
trusted_domain_deleteRemove. Any key that can see the entry.

Limits

PlanTrusted domains
Unclaimed sandbox agentsNone
Free10
Pro, Team and Scale500

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. Remove an entry, or upgrade, to add another.

Events

Every change is recorded in the event log 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.

Next