---
title: "Domains"
description: "`domains.list`, `list_all`, `iterate`, `get` and `update`."
url: "https://openemail.uk/docs/ruby/domains"
area: "Ruby"
category: "Mailbox"
---

# Domains

`domains.list`, `list_all`, `iterate`, `get` and `update`.

## Every method

**domains.rb**

```
page = client.domains.list
page.items.each { |row| puts "#{row[:domain]} #{row.dig(:sending, :canSend)}" }

domain = client.domains.get("b3e1f0a4-6c2d-4e8a-9f17-2d5c8a0b4e6f")
puts domain.dig(:receiving, :verified), domain.dig(:sending, :status)

domain[:addresses].each do |entry|
  puts "#{entry[:address]} #{entry[:enabled]}"
end
```

Receiving and sending are two independent facts and are returned as two Hashes. `receiving.verified` means the domain’s MX brings its mail here and its ownership challenge is published. `sending` reports the outbound signing check: `status` is `verified`, `pending`, `failed`, `no_identity` or `unknown`, and `canSend` says whether a send from the domain would be accepted right now. A negative verdict older than a day is treated as unknown rather than as a refusal, so branch on `canSend`, read as `domain.dig(:sending, :canSend)`, rather than on `status`.

`list` returns one `OpenEmail::Page` of domains in alphabetical order, and `list_all` returns them all in one Array. `iterate` yields them one at a time to a block. Without a block it returns an Enumerator.

**tracking_domain.rb**

```
domain_id = "b3e1f0a4-6c2d-4e8a-9f17-2d5c8a0b4e6f"

updated = client.domains.update(domain_id, trackingHost: "links.acme.com")
puts updated.dig(:tracking, :status), updated.dig(:tracking, :record, :name), updated.dig(:tracking, :record, :value)

client.domains.update(domain_id, trackingHost: nil)
```

`update` sets, checks again or removes the domain’s custom tracking domain, a subdomain such as `links.acme.com`, and returns the same Hash as `get`. `tracking` reports it on every read. Until a check passes, `tracking.status` is `pending` and tracked links and the open pixel keep using the default OpenEmail host. Once one passes, it is `active` and new mail from the domain uses the tracking domain for both.

> `get` also lists the addresses on the domain. `addresses.list` is the related call: the addresses this key may put in a From header, which is narrower, each with a `canSend` verdict. It returns an `OpenEmail::AddressBookPage`, which holds them in `addresses` rather than `items`, beside `domains` and `unrestricted`. Its `list_all` returns one `OpenEmail::AddressBook`.

> `app_host` is a namespace of its own, `client.app_host`. `get`, `set`, `verify` and `delete` read and change the web app address of the workspace, a subdomain such as `mailbox.acme.com` on one of these domains or any other domain the workspace controls, where its people sign in under the workspace brand. `set` returns the DNS records to publish, in `record` and, on a domain outside the workspace, `ownershipRecord`. `delete`, and a `set` that replaces an address, ask an OAuth app for a verification code: until it has one, the call raises a 403 whose `step_up_required?` is true.

> `branding` sets that brand. `get` reads the links to the mark, the logo, the logo for dark mode and the sign-in photo, the two fonts and the sign-in background. `update` changes the fonts and the background, `upload_image(variant, data, content_type: nil)` uploads one of the four images, and `remove_image(variant)` removes one. `variant` is `mark`, `wordmark`, `wordmark-dark` or `login-background`, and `OpenEmail::BRAND_IMAGE_VARIANTS` names them. `data` is a binary String, an IO or a Pathname. A Pathname such as `Pathname("logo.svg")`, a File or a Rails upload brings its type with it. Other bytes need `content_type:`, and an image without a type is refused with a 422 `invalid_image`. The logo is what brands the web app address and, on a paid plan, the emails sent for the workspace.

## Parameters: domains.get

- `id` (String, required): The id from `domains.list`, a UUID minted when the domain was added, not the hostname, so `get("example.com")` finds nothing. The lookup is scoped to the key’s own workspace as well as to the id, so another workspace’s domain is a 404, raised as `OpenEmail::NotFoundError`, rather than a 403. A nil or empty id raises ArgumentError before anything is sent.

## Parameters: domains.update

- `id` (String, required): The same domain id `get` takes. `domains:write` is the scope it needs.
- `trackingHost` (String or nil): A subdomain of the domain, at most 512 characters, such as `links.acme.com`. It is trimmed and lowercased, and a leading `https://` or `http://`, a path and a trailing dot are stripped. A new value is validated, saved and checked in the same call. The value the domain already has runs the check again, unless the last one was less than 30 seconds ago. Pass nil or an empty String to remove the tracking domain, and leave the field out to leave it alone.

> A refused host raises an `OpenEmail::ApiError` naming `trackingHost` in `param`: a 422 `invalid_tracking_host` for a name that cannot be used, such as one outside the domain, a 409 `domain_not_verified` for a new host while `receiving.verified` is false and the domain’s `_openemail-challenge` TXT record is not published yet, and a 409 `tracking_host_in_use` for a name another domain already uses, or when the tracking domain is managed by a different OpenEmail server. The 422 arrives as `OpenEmail::ValidationError` and each 409 as `OpenEmail::ConflictError`. A key limited to specific addresses gets a 422 `capability_unsupported`, because a tracking domain applies to every address on the domain.

> The patch is keyword arguments or one Hash, and its fields keep the API’s camelCase names, so `tracking_host:` is sent as written and refused with a 422 `unknown_parameter`. `update` also takes `catchAll`, `storageHost` for a files domain such as `files.acme.com`, and `dmarcPolicy`. Every field is optional and the method reference covers each one. The gem retries `update` like a read, because a repeat finds the host already set and at most checks it again.

## Response: a domain (domains.get)

- `object` (String): Always the string `domain`, on `list` rows and on this one alike.
- `id` (String): The domain’s UUID. Stable for the life of the row, and the only handle the other domain calls accept.
- `domain` (String): The bare hostname, lowercased: `example.com`. Unique across the whole product, one owner per domain, so two workspaces cannot both claim it.
- `receiving.verified` (Boolean): True once DNS showed the domain’s MX naming a host that brings its mail here and, where the row carries a challenge token, the matching `_openemail-challenge` TXT record. MX alone proves nothing, since every domain we receive for publishes the same hostnames, which is why the token exists, and why this flag is the gate inbound delivery checks before accepting mail.
- `receiving.verifiedAt` (String or nil): When verification passed, as an ISO 8601 String. It is nil while it has not, and `verified` is derived from exactly this column, so the two can never disagree.
- `receiving.catchAll` (Boolean): Whether any local-part is accepted. It is on by default for domains added since this became the rule. With it off, only addresses named on the domain are accepted and the rest are rejected at SMTP time, so the sender gets a bounce rather than silence.
- `receiving.lastCheckedAt` (String or nil): When DNS was last asked about this domain. It is nil when DNS was never asked, which reads very differently from a failure to somebody who added a domain a minute ago. Reading an unverified domain asks DNS again once the last check is more than 20 seconds old, so polling `get` is one way to wait for verification, and `verify` checks straight away.
- `receiving.error` (String or nil): Why the last check did not pass, in words the owner can act on: `No MX records yet. DNS changes can take a few minutes to spread.` is a typical one. It is nil once the check passes, and it is stored rather than derived, so a reload and the scheduled re-check say the same thing.
- `sending.status` (String): The outbound signing state as the last check saw it: `verified`, `pending`, `failed`, `no_identity` or `unknown`. It is read from the stored check, so `sending.checkedAt` says how old it is.
- `sending.canSend` (Boolean): Whether a send from this domain would be accepted right now. A negative verdict older than a day is treated as unknown rather than as a refusal, so this can be true while `status` is `pending`. Branch on this before a send: a false one means `emails.send` from this domain is refused with a 409 `domain_not_sendable`.
- `sending.checkedAt` (String or nil): When the signing state was last checked, as an ISO 8601 String. It is nil when it never was, which reads very differently from a failure.
- `sending.error` (String or nil): The last signing failure in words, or nil once it passes.
- `sending.note` (String): One of five sentences, chosen by `sending.status`, saying what that state means in words a domain owner can act on. It is prose for a human to read, so branch on `sending.canSend` rather than on this.
- `tracking` (Hash): The domain’s custom tracking domain, on `list` rows and on this one alike, and what `update` changes.
- `tracking.host` (String or nil): The tracking domain, such as `links.acme.com`, or nil when none is set.
- `tracking.status` (String): `none` means no tracking domain is set, `pending` means it has never passed a check, `active` means new mail uses it, and `failed` means it passed before and has since dropped out of use. An active host drops out after three failed checks in a row, or once its last passed check is more than 2 hours old.
- `tracking.active` (Boolean): True exactly when `status` is `active`, which is when tracked links and the open pixel in new mail from the domain use the host.
- `tracking.target` (String): The address the CNAME record points at, prepared for this tracking domain alone. It is an empty String while `host` is nil, and while the address for a new host is still being prepared.
- `tracking.record` (Hash or nil): The record to publish, a Hash with `type` (always `CNAME`), `name` and `value`, named after `host` with `target` as its value. It is nil when there is no tracking domain, and while the address for a new host is still being prepared, so `dig(:tracking, :record, :value)` reads it safely.
- `tracking.checkedAt` (String or nil): When the host was last checked, as an ISO 8601 String. It is nil until the first check.
- `tracking.verifiedAt` (String or nil): When a check last passed, as an ISO 8601 String. It is nil for a host that has never passed one.
- `tracking.error` (String or nil): What the last check found, in words the domain owner can act on. It is nil 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.
- `addresses` (Array<Hash>): Every address row on the domain, which is what `get` adds over a `list` row. It includes the rows delivery wrote itself under catch-all, and those stop being accepted the moment catch-all is turned off, so the Array is not a list of what will receive.
- `addresses[].address` (String): The full address, rebuilt from the stored local-part and the hostname and lowercased, so it always matches the `domain` above rather than drifting from it.
- `addresses[].enabled` (Boolean): False disables the address, and a disabled one is rejected even when catch-all is on. Every row is listed either way, so filter on this rather than reading the Array as the set of working addresses.
- `createdAt` (String): When the domain row was added, as an ISO 8601 String. It is not when the domain verified: that is `receiving.verifiedAt`, which can be nil while this is set.
