# Verify an email domain API Reference

Asks for the DNS records of the domain to be checked, once you have published them. The check
is asynchronous: the call returns `202` with the domain as it is now, usually still `PENDING`,
and its `status` changes to `VERIFIED` or `FAILED` when the check finishes. Read the domain with
`GET` to see the result. 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`.
- **`EMAIL_DOMAIN_NOT_READY` (`409`):** the domain is still registering (`dns_records: []`); repeat
  the call in a few seconds.
- **Sending service failure:** if the check cannot be requested the call fails with `502` and nothing changes.
- **Unpaid billing:** when the billing of the account is unpaid the call answers `402 PAYMENT_REQUIRED`.
- **Credentials:** 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/{email_domain_id}/verify

**Verify an email domain**

Asks for the DNS records of the domain to be checked, once you have published them. The check
is asynchronous: the call returns `202` with the domain as it is now, usually still `PENDING`,
and its `status` changes to `VERIFIED` or `FAILED` when the check finishes. Read the domain with
`GET` to see the result. 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`.
- **`EMAIL_DOMAIN_NOT_READY` (`409`):** the domain is still registering (`dns_records: []`); repeat
  the call in a few seconds.
- **Sending service failure:** if the check cannot be requested the call fails with `502` and nothing changes.
- **Unpaid billing:** when the billing of the account is unpaid the call answers `402 PAYMENT_REQUIRED`.
- **Credentials:** 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.
- **email_domain_id** (required) in path `string`: Identifier of the email domain.
- **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. |

### Responses

#### 202: Verification requested

**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": false,
    "dns_records": [
      {
        "type": "MX",
        "name": "send",
        "value": "mail.example.com",
        "priority": 10,
        "status": "PENDING"
      }
    ],
    "created_at": "2025-01-20T14:30:00Z"
  },
  "meta": {
    "timestamp": "2025-01-20T15:30:00Z",
    "request_id": "req_123"
  }
}
```

#### 400: The request target could not be read as declared, so nothing was looked up. Three causes,
all named in `error.details`: a path or query parameter whose value does not parse as its
type (`VALIDATION_ERROR` — a UUID that is not a UUID, an unknown enum value, an empty
element in a list such as `status=ISSUED,`), a required parameter that was not sent
(`MISSING_PARAMETER`), or a header that is meant to carry an identifier and does not
(`ACTIVE_COMPANY_HEADER_INVALID`). A list parameter sent entirely empty (`status=`) is
not an error: it is the same as omitting it.

The fault is in the target, not in the content — which is what `422` is defined over.


**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": "VALIDATION_ERROR",
    "message": "The parameter 'invoice_id' has an invalid type. Expected: UUID.",
    "details": {
      "field": "invoice_id",
      "invalid_value": "deliveries",
      "expected_format": "UUID"
    }
  },
  "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")

#### 404: `EMAIL_DOMAIN_NOT_FOUND` — no email domain with that id belongs to the account. A domain of another account, or one already removed, answers the same way.

**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": "EMAIL_DOMAIN_NOT_FOUND",
    "message": "No email domain with that id belongs to your account."
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

#### 409: `EMAIL_DOMAIN_NOT_READY` — the domain is still registering with the email sending service (`dns_records: []`).


**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 verification could not be requested from the email sending service.
Nothing changed. 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

## 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