Skip to the documentation
API

Update a domain

Sets, checks again or removes the domain’s custom tracking domain and custom files domain, the two things about a domain this API can change.

PATCHapi.openemail.uk/domains/{id}

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

PATCH /domains/{id}

Sets, checks again or removes the domain’s custom tracking domain and custom files domain, the two things about a domain this API can change.

The request

A domain can have one custom tracking domain and one custom files domain, each a subdomain of it that you choose, such as links.acme.com and files.acme.com, as soon as it is verified or its _openemail-challenge TXT record is published. It does not have to be receiving mail yet. Setting one prepares an address for that name alone, reported in target, and record is the CNAME record that points the name at it. Once a check passes, tracked links and the open pixel in new mail from the domain use https://links.acme.com/t/..., and the download links for files sent from it use https://files.acme.com/f/..., instead of the default host.

Parameters

trackingHoststring | null
The subdomain to use for tracked links and the open pixel, at most 512 characters. It is trimmed and lowercased, and a leading `https://` or `http://`, a path and a trailing dot are stripped before it is checked. A new value replaces the current tracking domain, the current value runs the check again, `null` or an empty string removes it, and leaving the field out leaves it alone.
storageHoststring | null
The subdomain to use for file download links, cleaned up the same way and held to the same 512 characters. A new value replaces the current files domain, the current value runs the check again, `null` or an empty string removes it, and leaving the field out leaves it alone.

The body is strict about keys and relaxed about how many you send. Any key other than trackingHost and storageHost is a 422 unknown_parameter, and a body carrying neither of them is a no-op that answers 200 with the domain as it stands. Both can go in one call, and they are applied in order, trackingHost first: a refused trackingHost stops the call before storageHost is touched, and a refused storageHost leaves a trackingHost change already made in place. Send them separately when either has to stand on its own.

Set a tracking domain and a files domain

Needs domains:write. Each host is validated, saved and checked in the same call, so the response already carries the result of that first check. It is the same body as GET /domains/{id}.

curl
curl -X PATCH "$OE/domains/b3e1f0a4-6c2d-4e8a-9f17-2d5c8a0b4e6f" -H "$AUTH" -H "Content-Type: application/json" \  -d '{ "trackingHost": "links.acme.com", "storageHost": "files.acme.com" }'
Response
{  "object": "domain",  "id": "b3e1f0a4-6c2d-4e8a-9f17-2d5c8a0b4e6f",  "domain": "acme.com",  "receiving": {    "verified": true,    "verifiedAt": "2026-08-14T10:02:00.000Z",    "catchAll": false,    "lastCheckedAt": "2026-08-29T06:00:00.000Z",    "error": null  },  "sending": {    "status": "verified",    "canSend": true,    "checkedAt": "2026-08-29T06:00:00.000Z",    "error": null,    "note": "Mail from this domain is signed and can be sent."  },  "tracking": {    "host": "links.acme.com",    "status": "pending",    "active": false,    "target": "oelinks3f9a1c7e2b8d4a60.edge.openemail.uk",    "record": { "type": "CNAME", "name": "links.acme.com", "value": "oelinks3f9a1c7e2b8d4a60.edge.openemail.uk" },    "checkedAt": "2026-08-29T06:05:12.000Z",    "verifiedAt": null,    "error": "links.acme.com does not resolve yet. Add a CNAME record named links.acme.com with the value oelinks3f9a1c7e2b8d4a60.edge.openemail.uk, then check again."  },  "storage": {    "host": "files.acme.com",    "status": "pending",    "active": false,    "target": "oefiles81c40d6b2f7e9a35.edge.openemail.uk",    "record": { "type": "CNAME", "name": "files.acme.com", "value": "oefiles81c40d6b2f7e9a35.edge.openemail.uk" },    "checkedAt": "2026-08-29T06:05:12.000Z",    "verifiedAt": null,    "error": "files.acme.com does not resolve yet. Add a CNAME record named files.acme.com with the value oefiles81c40d6b2f7e9a35.edge.openemail.uk, then check again."  },  "addresses": [    { "address": "[email protected]", "enabled": true }  ],  "createdAt": "2026-08-14T09:55:11.000Z"}

Publish tracking.record and storage.record at your DNS provider as plain CNAMEs, with any proxying turned off. The check resolves each name, then asks https://links.acme.com/t/v/<nonce> or https://files.acme.com/f/v/<nonce> for an answer signed by OpenEmail. A redirect fails the check, and so can a proxy in front of the name.

Once the record resolves, a check can report that the name points at OpenEmail and is waiting to be switched on. That is its HTTPS certificate being issued, which happens on our side, needs nothing from you and can take a little while. Once it is done, the next check that passes sets status to active.

If the address could not be prepared during the call, record is null, target is an empty string and error says it is being prepared. It is finished within a few minutes without another call, so read the domain again with GET /domains/{id} for the record.

The two names are independent. A call carrying one field leaves the other object exactly as it was, so setting up files later never disturbs a tracking domain that is already live.

Check again, or remove it

Send the host the domain already has to run the check now instead of waiting for the next scheduled one. When the last check, scheduled or not, ran less than 30 seconds ago, the call returns the stored state unchanged. Send null in a field to remove that name, and leave the other field out to keep the name it holds.

curl
curl -X PATCH "$OE/domains/b3e1f0a4-6c2d-4e8a-9f17-2d5c8a0b4e6f" -H "$AUTH" -H "Content-Type: application/json" \  -d '{ "trackingHost": null }'
tracking, after removal
{  "host": null,  "status": "none",  "active": false,  "target": "",  "record": null,  "checkedAt": null,  "verifiedAt": null,  "error": null}

Links in mail already sent keep the host they went out with, and that covers the download link on a file as much as a tracked link. After you remove or change a name, those links keep working as long as the old CNAME record stays in place. Setting a name up again can give it a different record, so publish the one the response reports.

The tracking object

hoststring | null
The tracking domain, or null when the domain has none.
status'none' | 'pending' | 'active' | 'failed'
`none` means no tracking domain is set. `pending` means one is set and has never passed a check. `active` means new mail uses it. `failed` means it passed a check before and has since dropped out of use.
activeboolean
True exactly when `status` is `active`, which is when tracked links and the open pixel in new mail from the domain use the host.
targetstring
The address the CNAME record points at, prepared for this tracking domain alone. It is an empty string while `host` is null, and while the address for a new host is still being prepared.
record{ type: 'CNAME'; name: string; value: string } | null
The record to publish, named after `host` with `target` as its value. Null when there is no tracking domain, and while the address for a new host is still being prepared.
checkedAtstring | null
When the host was last checked, ISO-8601. Null until the first check.
verifiedAtstring | null
When a check last passed, ISO-8601. Null for a host that has never passed one.
errorstring | null
What the last check found, in words the domain owner can act on. Null when the last check passed or none has run yet. A host that has failed one or two checks is still `active` and carries the reason here.

The storage object

The files domain reports into storage, field for field the same as tracking. Only what the name is used for differs: active there means the download links for files sent from the domain point at it.

hoststring | null
The files domain, or null when the domain has none.
status'none' | 'pending' | 'active' | 'failed'
`none` means no files domain is set. `pending` means one is set and has never passed a check. `active` means new mail uses it. `failed` means it passed a check before and has since dropped out of use.
activeboolean
True exactly when `status` is `active`, which is when the download links for files sent from the domain use the host.
targetstring
The address the CNAME record points at, prepared for this files domain alone. It is an empty string while `host` is null, and while the address for a new host is still being prepared.
record{ type: 'CNAME'; name: string; value: string } | null
The record to publish, named after `host` with `target` as its value. Null when there is no files domain, and while the address for a new host is still being prepared.
checkedAtstring | null
When the host was last checked, ISO-8601. Null until the first check.
verifiedAtstring | null
When a check last passed, ISO-8601. Null for a host that has never passed one.
errorstring | null
What the last check found, in words the domain owner can act on. Null when the last check passed or none has run yet. A host that has failed one or two checks is still `active` and carries the reason here.

How the host is checked

Both names are checked on the same schedule, and each is checked on its own.

  • A host that has not passed a check yet is checked every 2 minutes in its first hour, every 10 minutes in its first day, hourly in its first week and every 6 hours after that.
  • An active host is checked every 10 minutes, and a failed check on it is retried after 1 minute and then after 2.
  • An active host stops being used after three failed checks in a row, or once its last passed check is more than 2 hours old. New mail then goes back to the default host, and status reads failed until a check passes again. Checks carry on, further apart each time and at most an hour apart.

A tracking domain serves only tracking paths and a files domain serves only download paths, and each answers only for mail sent by the workspace that owns it.

Errors

StatustypecodeWhen
400invalid_request_errormalformed_jsonThe body is not valid JSON.
403permission_errorinsufficient_scopeThe key does not hold domains:write.
404not_found_errorresource_not_foundNo domain with that id in this workspace.
409conflict_errordomain_not_verifiedA new host was sent while receiving.verified is false and the domain’s _openemail-challenge TXT record is not published yet. param is the field it came in on, trackingHost or storageHost.
409conflict_errortracking_host_in_useAnother domain already uses the host as its tracking domain, the host is already in use as a files domain, or the domain’s tracking domain is managed by a different OpenEmail server. param is trackingHost.
409conflict_errorstorage_host_in_useThe same three cases for the files domain: another domain already uses the host as its files domain, the host is already in use as a tracking domain, or the files domain here is managed by a different OpenEmail server. param is storageHost.
422validation_errorinvalid_tracking_hostThe host is not a valid hostname, or is not allowed: it must be a strict subdomain of the domain, and cannot be the return path host bounce.<domain>, a name that belongs to OpenEmail or a domain set up to receive mail. param is trackingHost.
422validation_errorinvalid_storage_hostThe same rules, refused on the files domain. param is storageHost.
422validation_errorunknown_parameterA body key other than trackingHost and storageHost.
422validation_errorinvalid_parameterThe body is not a JSON object, or a field that is present is neither a string nor null, or runs past 512 characters. A body carrying neither field is not an error: it changes nothing and comes back 200.
422validation_errorcapability_unsupportedThe key is narrowed to single addresses rather than to this whole domain, and both names apply to every address on the domain. A key holding the domain in domainAllowlist may set them. param is domainAllowlist.