> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ruber.me/llms.txt
> Use this file to discover all available pages before exploring further.

# Domains & mailboxes

> Enrol a custom domain, read the DNS it needs, and create addresses on it once it verifies.

**Required permission:** Domains → Read or Write for the domain; Mailboxes →
Write to create an address.

The whole sequence is three calls and a wait for DNS:

<Steps>
  <Step title="Enrol the domain">
    `POST /v1/domains` returns the records to publish.
  </Step>

  <Step title="Publish the DNS, then poll">
    `GET /v1/domains/{id}` reports `outstanding` — how many records are still
    missing. Zero means it is done.
  </Step>

  <Step title="Create addresses">
    `POST /v1/mailboxes` works once `status` is `verified`.
  </Step>
</Steps>

## Enrol a domain

```
POST https://app.ruber.me/api/v1/domains
```

```bash theme={null}
curl -X POST https://app.ruber.me/api/v1/domains \
  -H "Authorization: Bearer $RUBER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "example.com"}'
```

```json theme={null}
{
  "id": "9f1c…",
  "name": "example.com",
  "status": "pending",
  "kind": "tenant",
  "ownership_verified_at": null,
  "mail_dns_verified_at": null,
  "outstanding": 4,
  "dns": [
    {
      "purpose": "ownership",
      "type": "TXT",
      "name": "_ruber-challenge",
      "value": "ruber-verify=8f14e45fce0a4e2a9b7f1d2c3b4a5e6f",
      "satisfied": false,
      "last_checked_at": null
    },
    { "purpose": "mx", "type": "MX", "name": "@", "value": "mx1.ruber.me", "priority": 10, "satisfied": false },
    { "purpose": "spf", "type": "TXT", "name": "@", "value": "v=spf1 include:amazonses.com ~all", "satisfied": false },
    { "purpose": "dmarc", "type": "TXT", "name": "_dmarc", "value": "v=DMARC1; p=quarantine; rua=mailto:dmarc@ruber.me", "satisfied": false }
  ]
}
```

**The `dns` array is the point of this endpoint.** Enrolling is the easy half;
publishing DNS is the work, and an answer that said only `"pending"` would leave
you with nothing to do next. Write those records to your DNS provider verbatim.

<Note>
  **`satisfied: false` with `last_checked_at: null` means "not looked at yet",
  not "missing".** They are different facts and only the second is a problem.

  DKIM records appear once Amazon SES has issued them, which is why a freshly
  enrolled domain lists four records and a checked one usually lists six.
</Note>

## Watch it verify

```
GET https://app.ruber.me/api/v1/domains        — all of them
GET https://app.ruber.me/api/v1/domains/{id}   — one, by id or by name
```

Poll the single-domain call and watch `outstanding` fall to zero. DNS
propagation is minutes to hours depending on your provider and TTL; there is no
webhook for it yet, so poll on a sensible interval rather than a tight loop.

Two verification timestamps are reported separately, because they fail for
different reasons and are fixed by different records:

|                         |                                                        |
| ----------------------- | ------------------------------------------------------ |
| `ownership_verified_at` | The `_ruber-challenge` TXT proved the domain is yours. |
| `mail_dns_verified_at`  | MX, SPF and DKIM are in place, so mail actually works. |

A domain can be owned and still unable to receive. Collapsing the two into one
"verified" flag would send you looking at the wrong record.

The ownership token is returned on `GET /v1/domains/{id}` for as long as the
challenge is live — so losing the `POST` response is recoverable, rather than
forcing you to delete the domain and enrol it again.

## Remove a domain

```
DELETE /v1/domains/{id}
```

Refused while the domain still has mailboxes or aliases, and the answer says
which — removing it first would orphan addresses that are still receiving mail.
Remove those, then the domain.

## Create an address

```
POST https://app.ruber.me/api/v1/mailboxes
```

```bash theme={null}
curl -X POST https://app.ruber.me/api/v1/mailboxes \
  -H "Authorization: Bearer $RUBER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com", "local_part": "billing", "display_name": "Billing"}'
```

| Field          |              |                                                |
| -------------- | ------------ | ---------------------------------------------- |
| `domain`       | **required** | The domain's id or its name.                   |
| `local_part`   | **required** | The part before the `@`.                       |
| `display_name` |              | What outgoing mail shows as the sender's name. |

`local_part` is lowercase letters, digits, dot, hyphen and underscore, starting
and ending alphanumeric, no consecutive dots, at most 64 characters. **No plus**
— plus-addressing is a suffix on an existing mailbox, not a name to own.

<Warning>
  **A key can only create Smart mailboxes.**

  A Private mailbox is encrypted to a key that only its owner's browser can
  generate, and the server never holds it. Creating one from an API would leave an
  address stuck in `provisioning` that nobody can finish, because finishing it
  means a person at a browser completing enrollment. It is refused rather than
  made broken.
</Warning>

The domain must be verified and not suspended. Reserved addresses — `postmaster`,
`abuse` and the other RFC-mandated names — are refused, and the first mailbox on
a domain automatically gets `postmaster` and `abuse` aliases pointing at it.

## Plan limits are the real ceiling

Every plan caps mailboxes and domains, and the database enforces it — so a key
cannot create a sixth mailbox on a five-mailbox plan however it asks:

```json theme={null}
{
  "error": {
    "code": "quota_exceeded",
    "message": "The personal plan includes 5 mailbox(es), and this organization already has 5. Upgrade to add more."
  }
}
```

Check [`GET /v1/usage`](/api/account#usage-and-limits) before a batch rather
than discovering the ceiling halfway through one.

## Not available yet

**Suspending or deleting a mailbox.** Both take an address out of service with
mail still in it, and the recovery path differs depending on why. They stay in
the dashboard.

**Changing a domain.** Nothing about an enrolled domain is editable: the name is
its identity, and the status is decided by what is in DNS rather than by anybody
asserting it. Remove and re-enrol.

**Triggering a DNS check.** Checks run on a schedule. There is no way to demand
one, because a caller in a loop would spend the rate limit re-asking a question
whose answer changes with DNS propagation, not with asking.

## Errors

| Code              | HTTP  | Cause                                                                                                 |
| ----------------- | ----- | ----------------------------------------------------------------------------------------------------- |
| `invalid_request` | `400` | A malformed name or local part, a domain already in use or being set up, or an address already taken. |
| `forbidden`       | `403` | Not your domain, a platform domain, a reserved address, or a domain whose DNS is not finished.        |
| `not_found`       | `404` | No domain of this organization with that id or name.                                                  |
| `quota_exceeded`  | `402` | Your plan's mailbox or domain limit.                                                                  |

See [Errors](/api/introduction#errors) for the full response shape.
