# Update a customer of a company partially API Reference

Updates only the fields present in the body, leaving every other field of the customer as it
is.

- **Null vs omitted:** a field sent as `null` is cleared, which is different from omitting
  it (see `PatchCustomerRequest`).
- **AEAT census:** the customer's Spanish tax identifier is only checked against the AEAT
  census when the request changes the `nif` or the `legal_name`. Editing anything else —
  phone, notes, address, billing emails — never asks the census, so a customer stored long
  ago stays editable even if its NIF is no longer listed.
  For a **legal entity** the census checks the CIF only and its `legal_name` is not
  verified, so the name never causes the rejection; only a **natural person**'s NIF is
  crossed with the name.
  Duplicate and format checks run on every update.
- **Only update verb:** this is the canonical way to edit a customer. There is no `PUT` of
  full replacement under the company, which would clear the fields you omit.


## PATCH /v1/companies/{company_id}/customers/{customer_id}

**Update a customer of a company partially**

Updates only the fields present in the body, leaving every other field of the customer as it
is.

- **Null vs omitted:** a field sent as `null` is cleared, which is different from omitting
  it (see `PatchCustomerRequest`).
- **AEAT census:** the customer's Spanish tax identifier is only checked against the AEAT
  census when the request changes the `nif` or the `legal_name`. Editing anything else —
  phone, notes, address, billing emails — never asks the census, so a customer stored long
  ago stays editable even if its NIF is no longer listed.
  For a **legal entity** the census checks the CIF only and its `legal_name` is not
  verified, so the name never causes the rejection; only a **natural person**'s NIF is
  crossed with the name.
  Duplicate and format checks run on every update.
- **Only update verb:** this is the canonical way to edit a customer. There is no `PUT` of
  full replacement under the company, which would clear the fields you omit.

### Authentication

Accepts any of:

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

### Parameters

- **company_id** (required) in path `string`: Unique identifier (UUID) of the company the operation acts on — its identifier, not its NIF. It is the only source of context: the account that owns it is derived from it, and the `BeeL-Active-Company` header plays no part. A company you do not reach answers `403`, and so does a company that does not exist, so the existence of a company in another account is never disclosed.
- **customer_id** (required) in path: Customer ID

### Request Body

Required.

**Content `application/json`:**

- **nif**: New Spanish Tax ID. Mutually exclusive with `alternative_id`. Omit to keep the current identifier; it cannot be cleared.
- **alternative_id**: New alternative identifier (non-Spanish customers). Mutually exclusive with `nif`. Omit to keep the current identifier.
- **legal_name** `string`: Customer legal name. Cannot be cleared. Matched against the AEAT census only for individuals; for a company the name is **not verified**.
- **trade_name** `string`: Customer trade name. Send `null` to clear it.
- **address**: Full address. Replaced as a whole (the address itself is not patched field by field) and cannot be cleared.
- **phone**: Phone number. Send `null` to clear it.
- **email** `string` (email): Main contact address. Invoice emails go here only when the customer has no `billing_emails`. Send `null` to clear it.
- **website** `string`: Website URL. Send `null` to clear it.
- **billing_emails** `array[Email]`: Addresses that receive the customer's invoice emails. When the send request names no `recipients` and the invoice has no `email_config` recipients, invoices go to these addresses, and not to `email`; `email` is used only when `billing_emails` is empty. Replaced as a whole; send `null` or `[]` to remove them all.
- **contact_person** `string`: Contact person name. Send `null` to clear it.
- **notes** `string`: Additional notes. Send `null` to clear it.
- **preferred_payment_method**: Default payment method. Replaced as a whole; omit it to keep the current one.
- **general_discount** `number`: General discount percentage. Send `null` to clear it.
- **active** `boolean`: Whether the customer is active or inactive.

**Example `update_customer_address`** — Update address:

```json
{
  "address": {
    "street": "Calle Nueva",
    "number": "456",
    "floor": "5º A",
    "postal_code": "28015",
    "city": "Madrid",
    "province": "Madrid",
    "country": "España"
  },
  "notes": "Cliente se ha mudado a nueva oficina"
}
```

**Example `update_customer_contact`** — Update contact:

```json
{
  "email": "nuevo-email@techsolutions.com",
  "phone": "+34912345999",
  "notes": "Cliente prioritario - Pago puntual. Nuevo contacto desde enero 2025."
}
```

**Example `deactivate_customer`** — Deactivate customer:

```json
{
  "active": false,
  "notes": "Cliente inactivo desde febrero 2025 - Sin actividad comercial"
}
```

### Responses

#### 200: Customer updated successfully

**Content `application/json`:**

- **success** (required) `boolean`: No description (example: true)
- **data** (required): The payload. An object for a single resource; an object holding the named collection (and its `pagination`) for a listing. Never a bare array at this level in v1.
- **meta** `ResponseMeta`
- **data** `Customer`

#### 400: `INVALID_JSON_FORMAT` — the body is not valid JSON, or a property has the wrong type or format
(a string where a number is expected, a date that does not parse, a malformed UUID, a boolean
that is not `true`/`false`). The `details` object follows `FieldDeserializationError`:
`field`, `invalid_value` and, where there is one, `expected_format` (or `allowed_values`, for a
boolean). A property the operation does not declare is not a format error: it is ignored.

A value outside an **enum**'s vocabulary is not answered here. The property is a well-formed
string that names nothing the operation knows, so it is judged as content: `422`
(`VALIDATION_ERROR`), with the same `FieldDeserializationError` shape in `details`
(`field`, `invalid_value`, `allowed_values`).


**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")
- **error**: No description

**Example:**

```json
{
  "success": false,
  "error": {
    "code": "INVALID_JSON_FORMAT",
    "message": "The field 'due_date' has an invalid date format: '2026-03-04fds'. Expected format: YYYY-MM-DD.",
    "details": {
      "field": "due_date",
      "invalid_value": "2026-03-04fds",
      "expected_format": "YYYY-MM-DD"
    }
  },
  "meta": {
    "timestamp": "2026-03-05T10: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"
  }
}
```

#### 403: Your account does not own or manage this company, or does not hold the required
access over it. A NIF that does not exist answers the same way.
That case answers `error.code` `ACTIVE_COMPANY_NOT_ACCESSIBLE`.


**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: There is no such customer in this company. It never refers to the NIF in the path:
a NIF you cannot reach answers `403`.


**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: Duplicate customer (same tax identifier), or another request modified this customer
first: `error.code` is `CONCURRENT_MODIFICATION` and no change was applied. Retry
the request over the current state of the customer.


**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: `VALIDATION_ERROR` — the body parses but a value is not acceptable. `details` is a flat map from
the offending property's name (in the contract's `snake_case`, nested paths joined with a dot,
e.g. `recipient.address.postal_code`) to a message describing what is wrong with it, one entry
per property. When the rejected value is one outside an enum's vocabulary, `details` instead
follows `FieldDeserializationError` (`field`, `invalid_value`, `allowed_values`).

Two more codes share this status for a value that passes the contract but does not fit
where it is stored: `VALUE_TOO_LONG` (a text longer than its column holds) and
`VALUE_OUT_OF_RANGE` (a number outside what its column holds). Nothing was written.

Other `error.code` values with this status:

- `RECIPIENT_UNREGISTERED_ID_MUST_BE_DNI_OR_NIE` — an `alternative_id` of type `NOT_REGISTERED` with country `ES` must be a DNI or NIE; a company (CIF) is always registered, send it as `nif`.


**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 provided data is not valid.",
    "details": {
      "legal_name": "The field 'legal_name' cannot be empty",
      "recipient.address.postal_code": "Contains invalid characters."
    }
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

#### 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"
  }
}
```

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

## PatchCustomerRequest

Partial update of a customer (RFC 5789). Only the fields present in the
body are touched:

- **field omitted** → the current value is kept;
- **field sent with a value** → replaced;
- **field sent as `null`** → cleared (only where documented as nullable
  below).

The resulting customer goes through the same validation as `PUT`
(AEAT census check for Spanish NIFs, duplicate identifier check
excluding this customer, field formats). The census check matches
`legal_name` only for individuals; for a company the name is
**not verified** and only the CIF decides.

- **nif**: New Spanish Tax ID. Mutually exclusive with `alternative_id`. Omit to keep the current identifier; it cannot be cleared.
- **alternative_id**: New alternative identifier (non-Spanish customers). Mutually exclusive with `nif`. Omit to keep the current identifier.
- **legal_name** `string`: Customer legal name. Cannot be cleared. Matched against the AEAT census only for individuals; for a company the name is **not verified**.
- **trade_name** `string`: Customer trade name. Send `null` to clear it.
- **address**: Full address. Replaced as a whole (the address itself is not patched field by field) and cannot be cleared.
- **phone**: Phone number. Send `null` to clear it.
- **email** `string` (email): Main contact address. Invoice emails go here only when the customer has no `billing_emails`. Send `null` to clear it.
- **website** `string`: Website URL. Send `null` to clear it.
- **billing_emails** `array[Email]`: Addresses that receive the customer's invoice emails. When the send request names no `recipients` and the invoice has no `email_config` recipients, invoices go to these addresses, and not to `email`; `email` is used only when `billing_emails` is empty. Replaced as a whole; send `null` or `[]` to remove them all.
- **contact_person** `string`: Contact person name. Send `null` to clear it.
- **notes** `string`: Additional notes. Send `null` to clear it.
- **preferred_payment_method**: Default payment method. Replaced as a whole; omit it to keep the current one.
- **general_discount** `number`: General discount percentage. Send `null` to clear it.
- **active** `boolean`: Whether the customer is active or inactive.

## Email

Email address (minimum valid email is 5 chars, e.g. a@b.co)

Type: `string` (email)

## SuccessResponse

The envelope every successful JSON response of the BeeL. API is wrapped in. There are no
bare resources in v1 and none are planned: the payload always hangs off `data`.

- A **single resource** is an object in `data`.
- A **collection** hangs off a named key inside `data`, together with its `pagination`,
  also inside `data` — `data: {invoices: [...], pagination: {...}}`.
- A collection carries `pagination` unless its operation declares itself a **closed
  catalogue**: a fixed, bounded list with nothing to page through. The declaration is
  explicit in the operation; a missing `pagination` is never something to infer.
- `GET /v1/accounts` pages by cursor (`data: {accounts: [...], next_cursor}`). It is a
  documented variant of pagination, not another envelope.

Putting the array straight into `data` with `pagination` as its sibling is the shape a v2
would adopt; v1 is not being flipped to it.

- **success** (required) `boolean`: No description (example: true)
- **data** (required): The payload. An object for a single resource; an object holding the named collection (and its `pagination`) for a listing. Never a bare array at this level in v1.
- **meta** `ResponseMeta`

## ResponseMeta

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

## Customer

- **id** (required) `string` (uuid): No description (example: "123e4567-e89b-12d3-a456-426614174000")
- **legal_name** (required) `string`: No description (example: "Empresa Cliente SL")
- **trade_name** `string`: No description (example: "EmpresaCliente")
- **nif** `NIF`: Spanish Tax ID (9 characters, uppercase only). Structural validation: - DNI: 8 digits + 1 letter (e.g., 12345678A) - NIE: X/Y/Z + 7 digits + 1 letter (e.g., X1234567A) - CIF: Organization letter + 7 digits + 1 control digit/letter (e.g., B12345674)
- **address** (required): Customer address as stored. Read shape: records registered before the street number was collected have none, so no field is guaranteed.
- **phone** `Phone`: A phone number, as the record holds it: digits, spaces, dashes, parentheses and an optional leading `+`, up to 20 characters. This is the schema a **response** carries, and the length above is the only rule it states. It deliberately does not repeat the character rule, because a number can reach a record through a path that predates that rule or never passed through this API at all — a payment provider's customer data, a bulk import. Read the field defensively and do not assume it parses. What a **request** has to satisfy is `PhoneInput`, which adds the rules this API enforces on the way in.
- **email** `string` (email): Main contact address (minimum valid email is 5 chars, e.g. a@b.co). Invoice emails go here only when the customer has no `billing_emails`.
- **website** `string`: Website URL
- **billing_emails** `array[Email]`: Addresses that receive the customer's invoice emails. When the send request names no `recipients` and the invoice has no `email_config` recipients, invoices go to these addresses, and not to `email`; `email` is used only when `billing_emails` is empty.
- **contact_person** `string`: No description (example: "María García")
- **notes** `string`: No description
- **preferred_payment_method** `PaymentInfo`
- **general_discount** `number`: No description
- **active** `boolean`: No description
- **created_at** (required) `string` (date-time): No description
- **updated_at** `string` (date-time): No description
- **alternative_id** `AlternativeIdentifier`: Alternative identifier for customers without Spanish Tax ID. ### VeriFactu rules (checked when you send it, `422` on violation) - `country_code` is required, except for `PASSPORT` (03) and `NOT_REGISTERED` (07), the two types AEAT accepts with `ES`: omitted, `ES` applies. Missing for any other type, the identifier is rejected with `ALTERNATIVE_ID_COUNTRY_REQUIRED`, and `error.details` names the field where it was sent: `alternative_id.country_code` on a customer, `recipient.alternative_id.country_code` on an invoice recipient. Same code in both. - If `country_code = ES`, then `type` **must** be `PASSPORT` (03) or `NOT_REGISTERED` (07) (`ALTERNATIVE_ID_SPAIN_INVALID_TYPE`). - If `type = NOT_REGISTERED` (07), then `country_code` **must** be `ES` (`ALTERNATIVE_ID_REQUIRES_SPAIN`), and `number` **must** be a Spanish DNI or NIE: a Spanish company is always registered (`RECIPIENT_UNREGISTERED_ID_MUST_BE_DNI_OR_NIE`). That DNI or NIE must not be one the AEAT census identifies: if it is, the identifier is rejected with `ALTERNATIVE_ID_ACTUALLY_IN_CENSUS` and has to be sent as `nif`. A customer is checked when it is saved; an invoice recipient, when the invoice is issued with VeriFactu. If the census does not answer, the identifier is not rejected. - If `type = NIF_IVA` (02), `country_code` **must** be an EU member state other than Spain (`ALTERNATIVE_ID_VAT_REQUIRES_EU_COUNTRY`), and `number` **must** have that country's EU VAT number structure as AEAT defines it: the country prefix (`EL` for Greece) followed by the national number, e.g. `FR40303265045`, `DE123456789`, `EL094014201` (`ALTERNATIVE_ID_VAT_INVALID_FORMAT`). Lowercase letters are accepted and stored in uppercase. A customer from outside the EU is identified with another type, such as `OTHER_DOCUMENT` or `COUNTRY_ID`. A `number` that is blank once trimmed is rejected with `ALTERNATIVE_ID_INVALID`. Well-formed is not the same as registered: an EU VAT number that is not in the VIES census is still rejected by VeriFactu after the invoice is issued. An identifier returned in a response is the one stored. A customer saved before a rule existed keeps its identifier and can still be read; issuing an invoice to it with an identifier that breaks these rules is rejected with the same code, before a number is used. ### Matrix of allowed combinations | `type`                 | `country_code = ES` | `country_code ≠ ES` | |------------------------|:-------------------:|:-------------------:| | `NIF_IVA` (02)         | ✗                   | EU member states only | | `PASSPORT` (03)        | ✓                   | ✓                   | | `COUNTRY_ID` (04)      | ✗                   | ✓                   | | `RESIDENCE_CERTIFICATE` (05) | ✗             | ✓                   | | `OTHER_DOCUMENT` (06)  | ✗                   | ✓                   | | `NOT_REGISTERED` (07)  | ✓                   | ✗                   |

## 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"})

## NIF

Spanish Tax ID (9 characters, uppercase only). Structural validation:
- DNI: 8 digits + 1 letter (e.g., 12345678A)
- NIE: X/Y/Z + 7 digits + 1 letter (e.g., X1234567A)
- CIF: Organization letter + 7 digits + 1 control digit/letter (e.g., B12345674)

Type: `string`

## Phone

A phone number, as the record holds it: digits, spaces, dashes, parentheses and an
optional leading `+`, up to 20 characters.

This is the schema a **response** carries, and the length above is the only rule it
states. It deliberately does not repeat the character rule, because a number can reach a
record through a path that predates that rule or never passed through this API at all —
a payment provider's customer data, a bulk import. Read the field defensively and do not
assume it parses.

What a **request** has to satisfy is `PhoneInput`, which adds the rules this API enforces
on the way in.

Type: `string`

## PaymentInfo

- **method**: Preferred payment method. Omitted, `BANK_TRANSFER` applies. If NONE is selected, no payment information will be shown on the invoice.
- **iban** `IBAN`: IBAN (International Bank Account Number). Required when payment method is BANK_TRANSFER.
- **swift** `SWIFT`: SWIFT/BIC code
- **payment_term_days** `integer`: Payment term in days. When marking an invoice as paid, every field of this object that travels replaces the stored one and every omitted field keeps its current value. (example: 30)

## AlternativeIdentifier

Alternative identifier for customers without Spanish Tax ID.

### VeriFactu rules (checked when you send it, `422` on violation)
- `country_code` is required, except for `PASSPORT` (03) and `NOT_REGISTERED` (07), the two
  types AEAT accepts with `ES`: omitted, `ES` applies. Missing for any other type, the
  identifier is rejected with `ALTERNATIVE_ID_COUNTRY_REQUIRED`, and `error.details` names
  the field where it was sent: `alternative_id.country_code` on a customer,
  `recipient.alternative_id.country_code` on an invoice recipient. Same code in both.
- If `country_code = ES`, then `type` **must** be `PASSPORT` (03) or `NOT_REGISTERED` (07)
  (`ALTERNATIVE_ID_SPAIN_INVALID_TYPE`).
- If `type = NOT_REGISTERED` (07), then `country_code` **must** be `ES`
  (`ALTERNATIVE_ID_REQUIRES_SPAIN`), and `number` **must** be a Spanish DNI or NIE: a
  Spanish company is always registered (`RECIPIENT_UNREGISTERED_ID_MUST_BE_DNI_OR_NIE`).
  That DNI or NIE must not be one the AEAT census identifies: if it is, the identifier is
  rejected with `ALTERNATIVE_ID_ACTUALLY_IN_CENSUS` and has to be sent as `nif`. A customer
  is checked when it is saved; an invoice recipient, when the invoice is issued with
  VeriFactu. If the census does not answer, the identifier is not rejected.
- If `type = NIF_IVA` (02), `country_code` **must** be an EU member state other than Spain
  (`ALTERNATIVE_ID_VAT_REQUIRES_EU_COUNTRY`), and `number` **must** have that country's
  EU VAT number structure as AEAT defines it: the country prefix (`EL` for Greece) followed
  by the national number, e.g. `FR40303265045`, `DE123456789`, `EL094014201`
  (`ALTERNATIVE_ID_VAT_INVALID_FORMAT`). Lowercase letters are accepted and stored in
  uppercase. A customer from outside the EU is identified with another type, such as
  `OTHER_DOCUMENT` or `COUNTRY_ID`.

A `number` that is blank once trimmed is rejected with `ALTERNATIVE_ID_INVALID`.

Well-formed is not the same as registered: an EU VAT number that is not in the VIES
census is still rejected by VeriFactu after the invoice is issued.

An identifier returned in a response is the one stored. A customer saved before a rule
existed keeps its identifier and can still be read; issuing an invoice to it with an
identifier that breaks these rules is rejected with the same code, before a number is used.

### Matrix of allowed combinations
| `type`                 | `country_code = ES` | `country_code ≠ ES` |
|------------------------|:-------------------:|:-------------------:|
| `NIF_IVA` (02)         | ✗                   | EU member states only |
| `PASSPORT` (03)        | ✓                   | ✓                   |
| `COUNTRY_ID` (04)      | ✗                   | ✓                   |
| `RESIDENCE_CERTIFICATE` (05) | ✗             | ✓                   |
| `OTHER_DOCUMENT` (06)  | ✗                   | ✓                   |
| `NOT_REGISTERED` (07)  | ✓                   | ✗                   |

- **type** (required) `string`: Identifier type. Use descriptive names: - **NIF_IVA**: EU VAT number (intra-community) — *only for an EU member state other than Spain, with that country's VAT number structure* - **PASSPORT**: Passport — *allowed for any country* - **COUNTRY_ID**: Country of residence ID — *not allowed when `country_code = ES`* - **RESIDENCE_CERTIFICATE**: Residence certificate — *not allowed when `country_code = ES`* - **OTHER_DOCUMENT**: Other supporting document — *not allowed when `country_code = ES`* - **NOT_REGISTERED**: Not registered in AEAT — *requires `country_code = ES`* **⚠️ DEPRECATED numeric codes** (will be removed in v2): 02, 03, 04, 05, 06, 07 — use the descriptive names above instead. — one of: NIF_IVA, PASSPORT, COUNTRY_ID, RESIDENCE_CERTIFICATE, OTHER_DOCUMENT, NOT_REGISTERED, 02, 03, 04, 05, 06, 07
- **number** (required) `string`: Identifier number. For `NIF_IVA`, the full EU VAT number with its country prefix (e.g. `FR40303265045`); see the VeriFactu rules on the parent schema.
- **country_code** `string`: ISO 3166-1 alpha-2 code of the country that issued the document. Required except for `PASSPORT` and `NOT_REGISTERED`, where omitting it means `ES`. Constrains the allowed `type` values; see the VeriFactu rules on the parent schema.

## IBAN

IBAN (International Bank Account Number).
Required when payment method is BANK_TRANSFER.

Type: `string`

## SWIFT

SWIFT/BIC code

Type: `string`


---

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