Docs

For an agent setting up mail

The setup API, for an agent — Claude Code, Cursor, a script — creating hello@ on a domain its person owns: the key, four calls, the records, the errors.

imado gives a domain your person already owns a working mailbox: hello@example.com, sent and received from a web inbox, with no mail server to run. This page is for an agent — Claude Code, Cursor, a script — setting that up on someone's behalf. The path to hello@ is four calls, one credential, and the errors you will meet; the rest of what a key can do is listed at the end.

What needs a person

Three things are theirs, not yours. Do them first, in this order, and stop at each until it is done.

  1. An account. Sign-up is a browser step at /sign-up: an email address they read (never one hosted on imado), a password, a verification link. An unverified account can do nothing.
  2. A key. Ask your person to open Account → API keys → New key at /account, name it for you ("Claude Code on the laptop"), and put the key in your environment — IMADO_API_KEY — and never in a file that gets committed. A key begins imado_sk_ and is shown once.
  3. DNS. A domain becomes a mailbox when a few records are published at its DNS provider. If you can reach that provider — Cloudflare, Vercel, Route 53, through their own tools — publish them yourself. If you cannot, hand your person the table this API gives you and wait.

The plan, if the free one does not cover them, is also theirs: a 409 with a limit in it is a conversation to have, not a call to retry.

The key

Every call carries it as a bearer: Authorization: Bearer imado_sk_….

A key does everything Setup does, as the person who minted it: list and add domains, read their records, ask for verification, create mailboxes. It never reads a mailbox, sees a bill, or opens anything across accounts — those doors answer 403 with one sentence, and that is the design, not a fault.

Three answers end the conversation:

  • 401 { "error": "unauthorized" } — the key is unknown, revoked or expired. Stop and ask your person for a new one. Never retry a 401.
  • 403 { "error": "forbidden", "message": "An API key can set up mail, not read it." } — you asked for something a key cannot have.
  • 503 { "error": "api-keys-disabled" } — the door is shut for everyone just now. Tell your person; try again later, not sooner.

The first thing to do with a key is prove it:

{}
{ "domains": [] }

An empty list is a good answer. A person with domains already gets each one with its id, name, receivingStatus, sendingStatus and a setup.label you can read out.

1. Add the domain

{ "name": "example.com" }
{
  "domain": {
    "id": "6e7968ab-385b-4593-80a9-a03c31ff76e9",
    "name": "example.com",
    "projectId": "795f6c5e-9a48-4f96-8397-7d5cd15bcba5"
  }
}

The first domain founds a project of the same name; keep the projectId, the mailbox call needs it. Lost it? The projects a key administers are one call away:

{}
{
  "projects": [
    {
      "id": "795f6c5e-9a48-4f96-8397-7d5cd15bcba5",
      "name": "example.com",
      "accountName": "Ada",
      "createdAt": "2026-09-19T08:48:49.898Z",
      "domains": 1
    }
  ]
}

The domain is the root name — example.com, not www.example.com — and it is the person's own.

What can go wrong here:

  • 422 invalid-name with a detail and a message — the name is not usable; say the message to your person.
  • 409 already-added — it is already in this person's list. Fetch it with GET /api/domains and carry on with its id.
  • 409 already-claimed — somebody else proved this domain first. Stop; this is a conversation between your person and us.
  • 409 mail-already-here with conflict naming the provider at the domain's MX — the domain already receives mail elsewhere (Google Workspace, say). imado would replace that. Ask your person, and only when they say so repeat the call with { "name": "example.com", "confirmedMxReplacement": true }.

2. Read the records

{}
{
  "domain": {
    "id": "6e7968ab-385b-4593-80a9-a03c31ff76e9",
    "name": "example.com",
    "projectId": "795f6c5e-9a48-4f96-8397-7d5cd15bcba5",
    "sendingStatus": "pending",
    "receivingStatus": "pending",
    "verifiedAt": null,
    "records": [
      {
        "type": "TXT",
        "host": "_imado-challenge",
        "name": "_imado-challenge.example.com",
        "value": "imado-verification=7zjvPGmA3bkJGXXj171RfXZiZq9hcrW5",
        "purpose": "ownership",
        "required": true,
        "status": "pending"
      }
    ],
    "setup": { "state": "unowned", "label": "Waiting for the record" }
  }
}

Each record is one row at the DNS provider: type, host (relative to the domain — what a provider's form asks for), value, and priority for an MX. name is the same host, fully qualified, for a provider that wants that instead. Publish every record whose required is true.

There is one record at first — the ownership challenge. Once it is seen, the answer grows: MX for receiving and the records for sending appear in the same list, with status "found" or "pending" beside each. Read the list again after ownership is proved and publish what is new.

3. Ask for verification

{}
{
  "domain": {
    "id": "6e7968ab-385b-4593-80a9-a03c31ff76e9",
    "name": "example.com",
    "projectId": "795f6c5e-9a48-4f96-8397-7d5cd15bcba5",
    "sendingStatus": "pending",
    "receivingStatus": "pending",
    "verifiedAt": null,
    "lastCheckedAt": "2026-09-19T08:48:49.948Z",
    "records": [
      {
        "type": "TXT",
        "host": "_imado-challenge",
        "name": "_imado-challenge.example.com",
        "value": "imado-verification=7zjvPGmA3bkJGXXj171RfXZiZq9hcrW5",
        "purpose": "ownership",
        "required": true,
        "status": "pending"
      }
    ],
    "setup": { "state": "unowned", "label": "Waiting for the record" }
  }
}

This looks up the records in DNS and moves the domain on when they are there. DNS takes minutes to propagate, so poll: no more often than every 30 seconds, and stop when receivingStatus is "verified" — that is the moment mail can arrive. sendingStatus follows on its own. A 429 means you asked too often; wait the Retry-After seconds it names.

If you stop polling, nothing is lost: imado checks every domain itself once a day and finishes the setup when the records appear, and tells your person by email what is still missing.

4. Create the mailbox

Once receivingStatus is "verified":

{ "domainIds": ["6e7968ab-385b-4593-80a9-a03c31ff76e9"], "localPart": "hello" }
{
  "mailboxId": "b661fda7-f52d-4dba-b4fa-85a8e52833f6",
  "addressId": "3f0f5528-6568-42c8-ba93-ebcef3bd04ee",
  "addresses": [
    {
      "addressId": "3f0f5528-6568-42c8-ba93-ebcef3bd04ee",
      "domainId": "6e7968ab-385b-4593-80a9-a03c31ff76e9",
      "isPrimary": true
    }
  ],
  "localPart": "hello"
}

localPart is the part before the @. The project's mailboxes, and how many the plan allows, are one read:

{}
{
  "mailboxes": [
    {
      "id": "b661fda7-f52d-4dba-b4fa-85a8e52833f6",
      "displayName": null,
      "status": "active",
      "addresses": [
        {
          "id": "3f0f5528-6568-42c8-ba93-ebcef3bd04ee",
          "localPart": "hello",
          "domainId": "6e7968ab-385b-4593-80a9-a03c31ff76e9",
          "domainName": "example.com",
          "isPrimary": true
        }
      ],
      "createdAt": "2026-09-19T08:48:50.016Z"
    }
  ],
  "limit": { "used": 1, "allowed": 10 }
}

What can go wrong when creating one:

  • 400 invalid-local-part with a detail — the name is not an address; try a plainer one.
  • 409 address-taken with the domainId — that address exists; choose another or ask.
  • 409 domain-unverified — verification has not finished; go back to step 3.
  • 409 mailbox-limit or 409 address-limit — the plan's limit. This belongs to your person.

5. Hand back

Tell your person three things: the address (hello@example.com), where to read it (/mail/<mailboxId> on imado), and that mail arrives once the domain's records are published and verified — which you either did or handed them the table for. Then stop.

In one place

  • GET /api/domains200 every domain of the person.
  • POST /api/domains201 the domain and its project; 422 invalid-name; 409 already-added, 409 already-claimed, 409 mail-already-here.
  • GET /api/domains/:id200 the domain with its records; 404 not-found.
  • POST /api/domains/:id/verify200 the domain, checked; 404 not-found; 429 too often.
  • GET /api/projects200 every project the person administers.
  • GET /api/projects/:projectId/mailboxes200 the project's mailboxes and its limit.
  • POST /api/projects/:projectId/mailboxes201 the mailbox; 400 invalid-local-part; 409 address-taken, 409 domain-unverified, 409 mailbox-limit, 409 address-limit.

A key reaches the rest of Setup too, as the person would. None of it is on the path to hello@; the two that delete are never called without your person's word.

  • GET /api/projects/:id200 the project; 404 not-found.
  • GET /api/projects/:id/domains200 every domain of the project, in whatever state.
  • PATCH /api/projects/:id200 the project renamed; 400 invalid-name; 404 not-found.
  • POST /api/projects/:projectId/domains/:id/reach200 what was done to bring the domain's records up to date at its provider, when imado can reach it; 404 not-found.
  • PATCH /api/projects/:projectId/mailboxes/:mailboxId200 the mailbox renamed; 400 invalid-display-name; 404 not-found.
  • DELETE /api/projects/:projectId/mailboxes/:mailboxId200 the mailbox stops receiving and its addresses come free; what it holds stays stored, for the person to read; 404 not-found.
  • POST /api/projects/:projectId/mailboxes/:mailboxId/addresses201 a second address on the mailbox; 400 invalid-local-part; 409 address-taken, 409 domain-unverified, 409 address-limit.
  • POST /api/projects/:projectId/mailboxes/:mailboxId/addresses/:addressId/primary200 that address is now the one mail goes out from; 404 not-found.
  • DELETE /api/projects/:projectId/mailboxes/:mailboxId/addresses/:addressId200 the address removed; 404 not-found; 409 last-address, 409 is-primary.

Everything else on /api is a person's: mail, billing, settings, keys. A key at any of them is told so, 403.