# Register an email domain API Reference

Registers a domain to send your account's emails from and returns the DNS records you have to
publish for it. The domain starts `PENDING`; publish the records and ask for the check with
`POST /v1/accounts/{account_id}/email-domains/{email_domain_id}/verify`. A domain that is not
`VERIFIED` 72 hours after it was registered is removed and its name is freed.

- **Add-on:** requires the custom email domain add-on, which is contracted with BeeL. Without
  it the call answers `403 FEATURE_NOT_AVAILABLE`.
- **Default:** the first domain of an account, or the new one when the account has no default
  domain, becomes the default; otherwise the default does not change.
- **`EMAIL_DOMAIN_INVALID` (`422`):** the text is not a full domain name. Internationalised names
  are accepted and stored in their ASCII form.
- **`EMAIL_DOMAIN_RESERVED` (`422`):** the domain cannot be used as a sending domain, for example because it belongs to BeeL
  or to a public mailbox provider.
- **`EMAIL_DOMAIN_UNAVAILABLE` (`409`):** the domain is already in use: registered by your account,
  by another account, or already held in the sending service BeeL uses. The answer is the same in
  every case and says nothing about who holds it.
- **While it registers:** a domain that is still registering shows `status: PENDING` with
  `dns_records: []`. Read it again in a few seconds; if it stays that way, remove it with `DELETE`
  and register it again.
- **`EMAIL_DOMAIN_LIMIT_REACHED` (`409`):** the account already holds as many domains as the
  add-on allows; `error.details.limit` carries that number.
- **Unpaid billing:** when the billing of the account is unpaid the call answers `402 PAYMENT_REQUIRED`.
- **Credentials:** it changes the real account, so it requires a live API key or a dashboard
  session. A test API key answers `403 LIVE_CREDENTIAL_REQUIRED`. Send an `Idempotency-Key` so a retry
  does not repeat the operation.


## POST /v1/accounts/{account_id}/email-domains

**Register an email domain**

Registers a domain to send your account's emails from and returns the DNS records you have to
publish for it. The domain starts `PENDING`; publish the records and ask for the check with
`POST /v1/accounts/{account_id}/email-domains/{email_domain_id}/verify`. A domain that is not
`VERIFIED` 72 hours after it was registered is removed and its name is freed.

- **Add-on:** requires the custom email domain add-on, which is contracted with BeeL. Without
  it the call answers `403 FEATURE_NOT_AVAILABLE`.
- **Default:** the first domain of an account, or the new one when the account has no default
  domain, becomes the default; otherwise the default does not change.
- **`EMAIL_DOMAIN_INVALID` (`422`):** the text is not a full domain name. Internationalised names
  are accepted and stored in their ASCII form.
- **`EMAIL_DOMAIN_RESERVED` (`422`):** the domain cannot be used as a sending domain, for example because it belongs to BeeL
  or to a public mailbox provider.
- **`EMAIL_DOMAIN_UNAVAILABLE` (`409`):** the domain is already in use: registered by your account,
  by another account, or already held in the sending service BeeL uses. The answer is the same in
  every case and says nothing about who holds it.
- **While it registers:** a domain that is still registering shows `status: PENDING` with
  `dns_records: []`. Read it again in a few seconds; if it stays that way, remove it with `DELETE`
  and register it again.
- **`EMAIL_DOMAIN_LIMIT_REACHED` (`409`):** the account already holds as many domains as the
  add-on allows; `error.details.limit` carries that number.
- **Unpaid billing:** when the billing of the account is unpaid the call answers `402 PAYMENT_REQUIRED`.
- **Credentials:** it changes the real account, so it requires a live API key or a dashboard
  session. A test API key answers `403 LIVE_CREDENTIAL_REQUIRED`. Send an `Idempotency-Key` so a retry
  does not repeat the operation.

### Authentication

Accepts any of:

- `ApiKeyAuth` (HTTP bearer, token format `beel_sk_*`)

### Parameters

- **account_id** (required) in path `string`: Your own account, or an account you provisioned. The account in the path, not the credential, decides which account the operation acts on. A `403` is returned when you do not reach it, the same response an account that does not exist gets.
- **Idempotency-Key** (optional) in header `string`: Idempotency key to prevent duplicates in sensitive operations. - Any unique client-generated string (e.g. an order id). A UUID also works but is not required - Allowed characters: letters, digits, `_` and `-` (max 255 chars) - Retrying with the same key replays the first response when it was a success (2xx) or a server error (5xx): same status and body, plus the header `Idempotency-Replay: true`. After a 5xx, check whether the operation took effect before retrying with a **new** key - A 4xx is not stored: the key is released, so the corrected request can reuse it - Stored responses expire 24 hours after processing The key is scoped per user and environment, and bound to the request body, so retrying after a network timeout replays the stored response instead of repeating the operation. | Status | Code | When | |---|---|---| | `400` | `INVALID_IDEMPOTENCY_KEY` | The key breaks the format rules above. | | `409` | `IDEMPOTENCY_KEY_PROCESSING` | The first request is still in flight. Wait for the `Retry-After` seconds (2) and retry with the same key. | | `409` | `IDEMPOTENCY_KEY_MISMATCH` | The key was already used with a **different** body. Use a new key. |

### Request Body

Required.

**Content `application/json`:**

- **domain** (required) `string`: Fully qualified domain name to send from. It is trimmed, lowercased and stored without a trailing dot; internationalised names are stored in their ASCII form. (example: "facturas.ejemplo.com")

**Example:**

```json
{
  "domain": "facturas.ejemplo.com"
}
```

### Responses

#### 201: Email domain registered

**Headers:**

- `Location` `string`: URI of the created resource — its canonical GET (`/v1/companies/{company_id}/...` or `/v1/accounts/{account_id}/...`).

**Content `application/json`:**

- **success** (required) `boolean`: No description (example: true)
- **data** (required) `EmailDomain`: A domain an account sends its emails from.
- **meta** `ResponseMeta`

**Example:**

```json
{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "domain": "facturas.ejemplo.com",
    "status": "PENDING",
    "is_default": true,
    "dns_records": [
      {
        "type": "MX",
        "name": "send",
        "value": "mail.example.com",
        "priority": 10,
        "status": "PENDING"
      },
      {
        "type": "TXT",
        "name": "send",
        "value": "v=spf1 include:example.com ~all",
        "status": "PENDING"
      }
    ],
    "created_at": "2025-01-20T14:30:00Z"
  },
  "meta": {
    "timestamp": "2025-01-20T14:30:00Z",
    "request_id": "req_123"
  }
}
```

#### 400: Bad request. From a dashboard session, a `BeeL-Environment` header whose value is not a
known environment answers `ENVIRONMENT_HEADER_INVALID`, with the header named in
`error.details` (`{"BeeL-Environment": "invalid"}`); it is never read as production.

`BUSINESS_RULE_VIOLATION` is the generic answer to an operation the resource does not
admit in its current state when that state has no code of its own. It carries no
`details`; read the resource again before retrying.


**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

**Example:**

```json
{
  "success": false,
  "error": {
    "code": "BAD_REQUEST",
    "message": "Invalid request"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

#### 401: Missing or invalid authentication. Like every other error, `message`/`detail` is
localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English when the
header is missing or asks for none of those.


**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

**Example:**

```json
{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Authentication is required to access this resource"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

#### 402: `PAYMENT_REQUIRED` — the billing of your account is unpaid (payment retries are exhausted), so the operation is refused until the outstanding invoice is settled.

**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

#### 403: The credential cannot reach the account in the path, lacks the `email-domains:write` scope,
is a test API key (`LIVE_CREDENTIAL_REQUIRED`), or the account does not have the custom
email domain add-on (`FEATURE_NOT_AVAILABLE`). An account you cannot reach and an account
that does not exist answer identically.


**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

#### 409: `EMAIL_DOMAIN_UNAVAILABLE` or `EMAIL_DOMAIN_LIMIT_REACHED`; `error.code` tells them apart.


**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

#### 422: `EMAIL_DOMAIN_INVALID` or `EMAIL_DOMAIN_RESERVED`; `error.code` tells them apart.


**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

#### 429: Rate limit exceeded. `RATE_LIMIT_EXCEEDED` when the request rate of your credential is
over its limit; `TOO_MANY_REQUESTS` when an email the operation sends is held back because
too many went out to that recipient in a short time. Either way, retry after the
`Retry-After` seconds.

`QUOTA_EXCEEDED` is the other `429`: the operation would go over a quota of your plan
for the current period (issued invoices, for instance). Retrying does not help until the
quota resets or the plan changes. `error.details` carries `feature_code`,
`current_usage`, `quota_limit` and `reset_date`, and `RateLimit-Reset` the seconds until
the reset when it is known.


**Headers:**

- `Retry-After` `integer`: Seconds until the rate limit resets
- `RateLimit-Limit` `integer`: Maximum requests allowed in the window
- `RateLimit-Remaining` `integer`: Remaining requests in the current window
- `RateLimit-Reset` `integer`: Seconds until the current window resets

**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

**Example:**

```json
{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests. Please try again in 60 seconds."
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

#### 500: Internal server error

**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

**Example:**

```json
{
  "success": false,
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "Internal server error"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

#### 502: `EXTERNAL_SERVICE_ERROR` — the domain could not be registered with the email sending service.
Nothing was registered. It is transient; repeat the request later.


**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

#### default: Any status code the operation does not list above. Every operation declares it, so a
generated client always has a branch to fall into and never loses the cause of a failure
it did not anticipate.

This is where the transport-level answers land — `405`, `406`, `415` and `429` — together
with any status a future version of the API starts returning. All of them carry the same
error envelope as the codes listed explicitly, so `error.code` is what tells them apart:
switching on the status code alone is not enough. See «Transport-level errors» in the
API description for when each one is produced.

A `402` carrying `SUBSCRIPTION_REQUIRED` lands here when the account has no active
subscription. `error.details` says why and what to do: `current_status` (absent if it
never had one), `required_permission` and `action`.

A `503` carrying `SERVICE_UNAVAILABLE` lands here too, with a `Retry-After` header: a
dependency of BeeL itself (the database, the email provider) was momentarily
unavailable and nothing was done. Retry after those seconds.

A `502` carrying `EXTERNAL_SERVICE_ERROR` also lands here: an outbound integration the
operation depends on failed or did not answer in time. It is a transient condition — retry
with the same `Idempotency-Key` where the operation accepts one.

One exception to the envelope: a failure of the network edge that never reaches the
application (`502`, `503`, `504`, `524`) is generated by Cloudflare and its body is not
BeeL's — it may not even be JSON. Treat those as "no answer", and retry.


**Content `application/json`:**

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

**Example:**

```json
{
  "success": false,
  "error": {
    "code": "UNSUPPORTED_MEDIA_TYPE",
    "message": "Unsupported media type: text/plain. Supported: application/json"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

---

# Related Schema Definitions

## CreateEmailDomainRequest

- **domain** (required) `string`: Fully qualified domain name to send from. It is trimmed, lowercased and stored without a trailing dot; internationalised names are stored in their ASCII form. (example: "facturas.ejemplo.com")

## EmailDomainResponse

- **success** (required) `boolean`: No description (example: true)
- **data** (required) `EmailDomain`: A domain an account sends its emails from.
- **meta** `ResponseMeta`

## EmailDomain

A domain an account sends its emails from.

- **id** (required) `string` (uuid): Identifier of the email domain. (example: "550e8400-e29b-41d4-a716-446655440000")
- **domain** (required) `string`: Fully qualified domain name, in lowercase. (example: "facturas.ejemplo.com")
- **status** (required) `EmailDomainStatus`: Verification status of an email domain, and of each of its DNS records. - PENDING: the records have not been detected yet, or verification is in progress. A domain that is not `VERIFIED` 72 hours after it was registered is removed - VERIFIED: the records were detected and the domain can be used to send - FAILED: verification did not succeed; check that the records are published as shown
- **is_default** (required) `boolean`: Whether this is the domain the account's emails are sent from by default. (example: true)
- **dns_records** (required) `array[DnsRecord]`: Records to publish in the domain's DNS, with the verification status of each. Empty (`[]`) while the domain is still registering, with `status: PENDING`: read it again in a few seconds, or remove it with `DELETE` if it stays empty.
- **created_at** (required) `string` (date-time): Moment the domain was registered.
- **verified_at** `string` (date-time): Moment the domain was verified. Absent until it is.
- **last_checked_at** `string` (date-time): Moment the records were last checked. Absent until the first check.

## ResponseMeta

- **timestamp** `string` (date-time): No description (example: "2025-01-15T10:30:00Z")
- **request_id** `string`: No description (example: "4bf92f3577b34da6a3ce929d0e0e4736")

## ErrorResponse

Error response shared by all BeeL. APIs.

The payload carries **two contracts at once** (additive, non-breaking):

- **Legacy** (`success`, `error.{code,message,details}`, `meta`) — kept
  intact for existing consumers.
- **RFC 9457** (`type`, `title`, `detail`, `instance`) — new fields
  for integrators following Problem Details for HTTP APIs. The
  `type` URI is the stable, shareable link to the error's
  documentation page (e.g. `https://docs.beel.es/errors/{code}`).

Future migration: the legacy fields will be deprecated via
`Deprecation`/`Sunset` headers after a sufficient adoption window,
and the response Content-Type will move to
`application/problem+json`.

- **success** (required) `boolean`: No description (example: false)
- **error** (required) `ErrorDetail`
- **meta** `ResponseMeta`
- **type** `string` (uri): Stable URI that identifies the problem type and where the integrator will find its documentation. (example: "https://docs.beel.es/errors/INVOICE_NO_LINES")
- **title** `string`: Short summary of the problem type. Stable between occurrences of the same `type`. (example: "INVOICE_NO_LINES")
- **detail** `string`: Specific message for this occurrence, localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English. Matches the legacy `error.message`. (example: "The invoice must have at least one line")
- **instance** `string` (uri-reference): URI that identifies the specific occurrence of the problem — typically the path of the affected resource. (example: "/v1/invoices/abc-123")

## ErrorDetail

- **code** (required) `string`: No description (example: "VALIDATION_ERROR")
- **message** (required) `string`: No description (example: "The provided data is not valid")
- **details** `object`: No description (example: {"field":"specific error message"})

## EmailDomainStatus

Verification status of an email domain, and of each of its DNS records.

- PENDING: the records have not been detected yet, or verification is in progress. A domain that
  is not `VERIFIED` 72 hours after it was registered is removed
- VERIFIED: the records were detected and the domain can be used to send
- FAILED: verification did not succeed; check that the records are published as shown

Type: `string` — one of: PENDING, VERIFIED, FAILED

## DnsRecord

A DNS record the domain owner has to publish for the domain to be verified.

- **type** (required) `DnsRecordType`: Type of a DNS record.
- **name** (required) `string`: Host name of the record, relative to the domain. (example: "send")
- **value** (required) `string`: Content the record must have. (example: "mail.example.com")
- **priority** `integer`: Priority of the record. Only present on `MX` records. (example: 10)
- **status** (required) `EmailDomainStatus`: Verification status of an email domain, and of each of its DNS records. - PENDING: the records have not been detected yet, or verification is in progress. A domain that is not `VERIFIED` 72 hours after it was registered is removed - VERIFIED: the records were detected and the domain can be used to send - FAILED: verification did not succeed; check that the records are published as shown

## DnsRecordType

Type of a DNS record.

Type: `string` — one of: MX, TXT, CNAME


---

Full OpenAPI spec: https://docs.beel.es/api/openapi