Sending domains

Sending from your own domain costs seven DNS records, published once. The loop below is designed to be driven by a script.

Setting up a domain, end to end

This is the loop an agent runs. Nothing here needs a human.
  1. Declare the domain.
    bash
    curl -X POST -H "x-api-key: $AGENTMAIL_API_KEY" -H "content-type: application/json" \
      -d '{"domain":"atelier-nord.fr"}' \
      "https://www.agentsmail.io/api/v1/domains"
  2. Read the records to publish. GET /api/v1/domains returns them. Seven in practice: one TXT for the ownership challenge, three DKIM CNAME, one MX and one TXT for the custom MAIL FROM, one TXT for DMARC. The challenge record comes first, and it is the one that decides usage. Its name is _agentmail-challenge.<domain> and its value is the organization's own token. DKIM alone is not enough: an identity already verified in the provider account returns SUCCESS to whoever declares it, so verified also requires this proof. A TXT name accepts several values — two organizations transferring a domain can publish side by side. The challenge record is served even when the provider is unreachable, because its value comes from our own database.
    json
    {
      "success": true,
      "data": [
        {
          "id": "…",
          "domain": "atelier-nord.fr",
          "dkimStatus": "PENDING",
          "mailFromStatus": "PENDING",
          "verified": false,
          "ownershipProved": false,
          "lastCheckedAt": "2026-08-12T12:00:00.000Z",
          "sesUnavailable": false,
          "dnsRecords": [
            {"type": "TXT", "name": "_agentmail-challenge.atelier-nord.fr", "value": "Xy7pQ2rL9mNv4KtB1sZaWg"},
            {"type": "CNAME", "name": "abc._domainkey.atelier-nord.fr", "value": "abc.dkim.amazonses.com"},
            {"type": "MX", "name": "mail.atelier-nord.fr", "value": "feedback-smtp.eu-west-1.amazonses.com", "priority": 10},
            {"type": "TXT", "name": "mail.atelier-nord.fr", "value": "v=spf1 include:amazonses.com ~all"},
            {"type": "TXT", "name": "_dmarc.atelier-nord.fr", "value": "v=DMARC1; p=none;"}
          ]
        }
      ]
    }
  3. Publish them at the registrar. That part is outside the API.
  4. Re-check. POST /api/v1/domains/{domainId}/verify asks the email provider again and stores the answer. Poll it: DNS propagation takes from a few minutes to 72 h.
  5. Confirm. GET /api/v1/me now answers "ready": true.
DKIM and ownership gate sending, together. mailFromStatus and the DMARC record improve deliverability — a pending MAIL FROM never blocks a send. The verified field means usable: dkimStatus === "SUCCESS" and the ownership challenge proved, on the same domain. Read verified and you never have to reimplement the rule; read ownershipProved and dkimStatus to know which of the two records is still missing.

Ownership is proved once, and never revoked

The proof is a dated event, not a continuous state. Once a domain is proved, removing the TXT record later does not revoke it — otherwise any DNS outage would cut off sending. Re-publishing the challenge for another organization transfers the domain: a proved domain belongs to exactly one organization at a time. Checking is synchronous only: it happens when you call verify or open the domain screen. There is no background job.

The records are never stored

The provider records are read back on every call, because they change if the identity is recreated. Do not cache them: a stale copy sends someone to publish records that no longer verify anything. The challenge record is the exception in origin only — its value is ours, which is why it survives a provider outage — but it is served the same way, never cached by you.

When the email provider is unreachable

Reads do not fail. GET /api/v1/domains returns the last known state with "sesUnavailable": true and its lastCheckedAt. verify does fail — checking is precisely what you asked for, and pretending otherwise would be lying.