Chapter 2, domains
Connect a domain
Your agents can work forever on free <slug>.mail.aiaxmail.com addresses. You connect your own domain when you want the From line to carry your brand, which is usually the day the first real customer sees a message.
2.1 Three topologies, chosen explicitly
A free-form domain field makes this decision implicitly and silently, which is how a company ends up with agent mail and company mail behind the same MX record. You choose one of three, and the record set is generated from the choice.
| Topology | Sends as | Receives at | Touches your MX | Records |
|---|---|---|---|---|
| agents.acme.com Isolated subdomain default |
@agents.acme.com | agents.acme.com | no | 7 |
| acme.com Send only |
@acme.com | agents.acme.com | no | 5 |
| acme.com Full migration |
@acme.com | acme.com | replaces it | 7 |
The send-only path is the one most European companies want and the one nobody documents. SPF and DKIM go on the apex, your inbound MX there is neither created nor changed, and Microsoft 365 or Google Workspace keeps every inbound message it has today. Replies land on the subdomain, where your agents already are.
2.2 The record set
Seven records for the default topology. Every value below is the live value for agents.acme.com, in both forms, because providers disagree about which one they want.
| Row | Type | Name, relative | Value | Needed for |
|---|---|---|---|---|
| 01 | MX | agents | inbound-smtp.eu-north-1.amazonaws.com priority 10 | Receiving |
| 02 | CNAME | k7r4qm2a._domainkey.agents | k7r4qm2a.dkim.amazonses.com | Sending |
| 03 | CNAME | p3xd91vc._domainkey.agents | p3xd91vc.dkim.amazonses.com | Sending |
| 04 | CNAME | z8h5tn6b._domainkey.agents | z8h5tn6b.dkim.amazonses.com | Sending |
| 05 | MX | mail.agents | feedback-smtp.eu-north-1.amazonses.com priority 10 | Bounces |
| 06 | TXT | mail.agents | v=spf1 include:amazonses.com -all | Sending |
| 07 | TXT | _dmarc.agents | v=DMARC1; p=quarantine; rua=mailto:dmarc@acme.com | Policy |
DKIM is three CNAME records, on purpose
The alternative is one TXT record containing an RSA public key, which is longer than the 255 character limit for a single character-string. Every provider then handles the split differently, and a key split wrongly produces two TXT records instead of one, which fails silently at the receiving end.
Three CNAMEs have no length, no quoting and no splitting. If you have ever spent an afternoon on "first""second" with no space between the quotes, that afternoon cannot happen here.
Both name forms, always
curl -X POST https://api.aiaxmail.com/v1/domains \
-H "authorization: Bearer $AIAX_MAIL_KEY" \
-d '{"domain":"agents.acme.com","topology":"subdomain"}'
# every record carries both forms and a provider hint
{
"records": [{
"type": "CNAME",
"name_relative": "p3xd91vc._domainkey.agents",
"name_fqdn": "p3xd91vc._domainkey.agents.acme.com",
"value": "p3xd91vc.dkim.amazonses.com",
"provider_hint": "cloudflare_appends_zone",
"status": "missing",
"observed": null,
"checked_at": null
}]
}const domain = await mail.domains.create({
domain: "agents.acme.com",
topology: "subdomain"
});
for (const r of domain.records) {
console.log(r.type, r.nameRelative, r.value, r.status);
}
// verification runs on its own. this waits for it.
const verified = await mail.domains.waitUntilVerified(domain.id, { timeout: "30m" });domain = mail.domains.create(domain="agents.acme.com", topology="subdomain")
for r in domain.records:
print(r.type, r.name_relative, r.value, r.status)
verified = mail.domains.wait_until_verified(domain.id, timeout="30m")2.3 Verification
Verification starts by itself the moment the records are generated. There is no button you have to find, and no state where the product is waiting for you without saying so.
| State | What it means | Who acts |
|---|---|---|
| waiting_for_dns | Nothing resolves yet. Normal until you save the records. | You |
| checking_nameservers | We are querying your authoritative nameservers directly, not a cache. | Us |
| found_but_wrong | A record resolves with a value we did not ask for. The observed value is returned. | You |
| propagating | Correct at the authoritative nameserver, not yet everywhere. | Nobody |
| authorising | Amazon SES is authorising the identity. Nothing left at your provider. | Us |
| verified | Sending and receiving are live. The domain.verified event fires. | Nobody |
Measured, not defensive
Median time to verified is , 95th percentile , over (). These are a rolling statistic we publish at /data/verification-stats.json and recompute as domains verify, and this sentence is generated from it.
We do not publish "up to 48 hours", because it is true of nothing.
2.4 When it does not verify
Every failure below is one the console detects and names, with the observed value beside the expected one. You should never be reading this page to work out what went wrong. It is here because someone will read it anyway.
- Your provider appended the zone. Cloudflare, GoDaddy and Namecheap add the zone to a value that does not end in a dot. The fix is the trailing dot, and the console shows you the exact string.
- You pasted the fully qualified name into a relative field. You end up with agents.acme.com.acme.com. Use the toggle rather than editing by hand.
- The DKIM selector already exists. An old CNAME on the same selector wins. Delete it first.
- Your nameservers are somewhere else. The registrar's DNS panel is not always the authoritative one. We name the nameservers we queried so you can tell.
- The record is correct and nothing happens. Propagation. The record book shows the last check and the next one, so you can see it is not stuck.
3. The error contract
Every error, on every endpoint, returns the same four fields. The client for this API is increasingly a language model, and a model can recover from an error that tells it what to do next.
| Field | Contract |
|---|---|
| code | Stable, snake case, part of the API. Branch on this. |
| message | Written for a human. Will be reworded without notice. Never branch on it. |
| fix | The next call or the next action, concretely, with the values filled in. |
| docs | A deep link to the section that explains it. |
{ "code": "verification_required", "message": "This key can receive mail but cannot send yet.", "fix": "POST /v1/agent/verify with the 6 digit code sent to y***@acme.com", "docs": "https://aiaxmail.com/docs/errors#verification_required", "retry_after": null}There is no state in which a send is accepted and then quietly discarded. If the account cannot send, the call fails, with a code, in front of the agent that made it.
4. Reading this as an agent
If you are a coding agent integrating this API, you do not need this page. Take the plain text instead.
| Path | What it is | Served as |
|---|---|---|
| /llms.txt | The map. Every page, one line each, with its purpose. | text/plain |
| /llms-full.txt | The whole reference with every sample inline. | text/plain |
| /docs/domains.md | This page as markdown. Every page works this way. | text/markdown |
| /openapi.json | Includes the Idempotency-Key header, so generated clients expose it. | application/json |
Paste this into your coding agent
You are integrating AiAx Mail, an email API that gives agents real mailboxes.
Read https://aiaxmail.com/llms.txt first, then fetch only the pages you need.
Rules that will save you a round trip:
- Branch on error.code, never on error.message. Every error carries code, message, fix, docs.
- A new key can receive immediately and cannot send until a human completes OTP verification.
A send before that returns 403 verification_required. Do not retry it. Surface the fix.
- Sends take an Idempotency-Key header. Creates take client_id. Reusing either with a
different body returns 409.
- Custom domains go on a subdomain (agents.example.com) by default. Do not register an apex
unless the human has explicitly asked for the send-only or full migration topology.
- The region is eu-north-1. Do not assume a US endpoint.Design artefact, Lane A. This documentation page is a layout study. The endpoints, records and numbers on it describe the intended contract, not a service you can call today.