Skip to the documentation
API

Add a domain

Adds a domain to the workspace and returns it with every DNS record it needs. The domain starts unverified, with its catch-all on, and verifies on its own once the records answer in public DNS.

POST/domains

Runs the real call against your workspace, with your own key.

POST /domains

Adds a domain to the workspace and returns it with every DNS record it needs. The domain starts unverified, with its catch-all on, and verifies on its own once the records answer in public DNS.

The request

Parameters

domainstringrequired
A bare domain such as `acme.com` or `mail.acme.com`. It is trimmed and lowercased, and an international name is stored in its ASCII form. A scheme, a path or an address is refused.

Example

Needs domains:write, and a key with no address or domain restriction, because a new domain reaches the whole workspace.

curl
curl -X POST "$OE/domains" -H "$AUTH" -H "Content-Type: application/json" \  -d '{ "domain": "acme.com" }'
Response
{  "object": "domain",  "id": "b3e1f0a4-6c2d-4e8a-9f17-2d5c8a0b4e6f",  "domain": "acme.com",  "receiving": {    "verified": false,    "verifiedAt": null,    "catchAll": true,    "lastCheckedAt": "2026-09-25T10:00:02.000Z",    "error": "No MX records yet. DNS changes can take a few minutes to spread."  },  "sending": {    "status": "no_identity",    "canSend": false,    "checkedAt": null,    "error": null,    "note": "Sending is not set up yet. Publish the signing record listed for this domain, and sending switches on shortly after it answers in DNS."  },  "tracking": { "host": null, "status": "none", "active": false, "target": "", "record": null, "checkedAt": null, "verifiedAt": null, "error": null },  "storage": { "host": null, "status": "none", "active": false, "target": "", "record": null, "checkedAt": null, "verifiedAt": null, "error": null },  "addresses": [],  "createdAt": "2026-09-25T10:00:00.000Z",  "records": [    { "type": "MX", "name": "acme.com", "value": "inbound.mail.example", "priority": 10, "purpose": null, "status": "missing" },    { "type": "TXT", "name": "acme.com", "value": "v=spf1 include:_spf.openemail.uk ~all", "priority": null, "purpose": null, "status": "missing" },    { "type": "TXT", "name": "_openemail-challenge.acme.com", "value": "oe-verify=4f1c9a2b7e8d6053a1c4", "priority": null, "purpose": "Proves you own this domain", "status": "missing" }  ],  "dmarc": null}

The example trims records. A real response lists every record the domain needs, including DMARC, the signing CNAMEs and the return-path records, and GET /domains/{id} describes each field.

Publish the records exactly as given, then call POST /domains/{id}/verify or wait: the domain is checked again whenever it is read and by a background sweep, and domain.verified fires the moment it passes.

When it is refused

  • 409 domain_already_added: this workspace owner already has the domain.
  • 409 domain_claimed: another account has it.
  • 409 related_domain_owned: a parent or a subdomain of it belongs to another account, so it cannot be split between the two.
  • 409 operator_domain: the domain belongs to OpenEmail itself.
  • 403 domain_allowance_reached: the plan holds no more domains. The message names the plan that holds more.
  • 422 capability_unsupported: the key is limited to particular addresses or domains.

Your inbox,
on your own terms.

Email infrastructure for businesses, AI, agents and personal email. Built for scale, privacy and control. Everything email should have had from day one.

OpenEmail

Email infrastructure for businesses, AI, agents and personal email. Built for scale, privacy and control. Everything email should have had from day one.

© 2026 OpenEmail. All rights reserved.