AiAx Mail Docs
Site closed preview

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.

The record tables and record counts below are generated, not retyped: they, the zone file the console hands you and the payload the API returns all come from the same connection logic (dnsRecordsFor in src/domain.ts), via npm run docs:dns. They cannot drift from what the product actually asks for, because there is nothing here typed by hand.

2.1 Two 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 two, and the record set is generated from the choice. Receiving on your apex is refused by construction - there is no "full migration" option and no way to force one.

TopologySends asReceives atTouches your MXRecords
records.example.com
Isolated subdomain
default
@records.example.com records.example.com no 8
example.com
Sending only
@example.com records.example.com no 7

To send as your apex without receiving there, use "mode": "sending_only", which publishes SPF and DKIM on your apex and no MX on the apex itself. Its one MX sits on bounce.<your apex>, the host that exists only so bounces reach us; nothing addressed to you is delivered there. Microsoft 365 or Google Workspace keeps every inbound message it has today, and replies land on the subdomain, where your agents already are.

2.2 The record set

8 records for the default topology. Every value below is the live value for records.example.com, in both forms, because providers disagree about which one they want.

RowTypeName, relativeValueNeeded for
01TXT_agentmail-challenge.recordsaiax-domain-verification=069a6627d2619e84712c65c078d24d01Ownership
02MXrecordsinbound-smtp.eu-north-1.amazonaws.com priority 10Receiving
03TXTrecordsv=spf1 include:amazonses.com ~allSending
04CNAMEk7r4qm2a9x3wfhb2n8v6s1tzq4pldc7e._domainkey.recordsk7r4qm2a9x3wfhb2n8v6s1tzq4pldc7e.dkim.amazonses.comSending
05CNAMEp3xd91vc5m0ky7wa2r8h4bfzn6qte1sj._domainkey.recordsp3xd91vc5m0ky7wa2r8h4bfzn6qte1sj.dkim.amazonses.comSending
06CNAMEz8h5tn6b1qw3xr9m0v7dcp4ley2ksfj8._domainkey.recordsz8h5tn6b1qw3xr9m0v7dcp4ley2ksfj8.dkim.amazonses.comSending
07MXbounce.recordsfeedback-smtp.eu-north-1.amazonses.com priority 10Bounces
08TXTbounce.recordsv=spf1 include:amazonses.com ~allSending

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":"records.example.com","topology":"subdomain"}'

# every record carries both forms and a provider hint
{
  "records": [{
    "type": "CNAME",
    "name_relative": "p3xd91vc5m0ky7wa2r8h4bfzn6qte1sj._domainkey.records",
    "name_fqdn": "p3xd91vc5m0ky7wa2r8h4bfzn6qte1sj._domainkey.records.example.com",
    "value": "p3xd91vc5m0ky7wa2r8h4bfzn6qte1sj.dkim.amazonses.com",
    "provider_hint": "cloudflare_appends_zone",
    "status": "missing",
    "observed": null,
    "checked_at": null
  }]
}

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.

StateWhat it meansWho acts
waiting_for_dnsNothing resolves yet. Normal until you save the records.You
checking_nameserversWe are querying your authoritative nameservers directly, not a cache.Us
found_but_wrongA record resolves with a value we did not ask for. The observed value is returned.You
propagatingCorrect at the authoritative nameserver, not yet everywhere.Nobody
authorisingAmazon SES is authorising the identity. Nothing left at your provider.Us
verifiedSending and receiving are live. The domain.verified event fires.Nobody

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 records.example.com.example.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.

FieldContract
codeStable, snake case, part of the API. Branch on this.
messageWritten for a human. Will be reworded without notice. Never branch on it.
fixThe next call or the next action, concretely, with the values filled in.
docsA deep link to the section that explains it.
403 verification_required
{  "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 c***@example.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.

PathWhat it isServed as
/llms.txtThe map. Every page, one line each, with its purpose.text/plain
/llms-full.txtThe whole reference with every sample inline.text/plain
/docs/domains.mdThis page as markdown. Every page works this way.text/markdown
/openapi.jsonIncludes the Idempotency-Key header, so generated clients expose it.application/json

Paste this into your coding agent

prompt
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 separate subdomain (records.example.com in these examples) by default. Apex receiving is
  refused; there is no "full migration" topology. If the human wants @example.com on the
  From line, use sending_only instead, which publishes SPF and DKIM on the apex and no MX there.
- The region is eu-north-1. Do not assume a US endpoint.

AiAx Mail is in closed preview. 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.