# Update a customer partially (deprecated) 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`). The result goes through the same validation as `PUT`.
- **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 never
  asks the census.
  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.
- **Deprecated:** use `PATCH /v1/companies/{company_id}/customers/{customer_id}`, which
  behaves identically.

**Retires on 9 December 2026.** See the [migration guide](https://docs.beel.es/changelog/resources-under-the-nif) for what moved where and what changes when you switch.


## PATCH /v1/customers/{customer_id}

**Update a customer 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`). The result goes through the same validation as `PUT`.
- **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 never
  asks the census.
  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.
- **Deprecated:** use `PATCH /v1/companies/{company_id}/customers/{customer_id}`, which
  behaves identically.

**Retires on 9 December 2026.** See the [migration guide](https://docs.beel.es/changelog/resources-under-the-nif) for what moved where and what changes when you switch.

**Deprecated.**

### Authentication

Accepts any of:

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

### Parameters

- **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.

### 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: Authenticated but not allowed. Fourteen causes shared by every operation, told apart by
`error.code`; an operation that adds a cause of its own names it in its `403`. You can
branch on them exhaustively.

- `INSUFFICIENT_SCOPE` — the credential lacks a scope the operation requires;
  `error.details.missing_scopes` names them as a single comma-separated string (for
  example `"invoices:write,customers:read"`), not as an array; `required_scopes` has the
  same shape and lists every scope the operation needs. Retrying will not help: mint a key
  that holds them.
- `COMPANY_READ_ONLY` — the scope is there, but your access level over that NIF only
  lets you read it.
- `ACCOUNT_MANAGEMENT_FORBIDDEN` — the scope is there, but your role over the account,
  or the management relationship you hold over it, does not cover this operation.
- `ACCOUNT_NOT_ACCESSIBLE` — the account is not yours to reach, which is also the answer
  when it does not exist, so existence is never disclosed.
- `ACTIVE_COMPANY_NOT_ACCESSIBLE` — the same, for a NIF: the company in the path, or the
  one named by `BeeL-Active-Company`, is not one this credential may reach — and again,
  this is also the answer when it does not exist.
- `COMPANY_ACCESS_REVOKED` — your access to the NIF was withdrawn while the request was
  already in flight, so the write was rejected and nothing was recorded. Retrying will
  not help until the access is granted again.
- `LIVE_CREDENTIAL_REQUIRED` — the operation changes the real account and the call came
  from a test API key (`beel_sk_test_…`). Use your live key or the dashboard.
- `FEATURE_NOT_AVAILABLE` — your subscription does not include the entitlement the
  operation needs; `error.details.feature_code` names it. This gate runs **before** the
  scope gate, so for such an operation you never see `INSUFFICIENT_SCOPE` first.
- `NO_ACTIVE_ACCOUNT` — the credential does not resolve to an account, so no scope can be
  evaluated against one. Fail-closed, not a permission that can be granted to you.
- `OPERATION_REQUIRES_SESSION` — the operation is available only from the web session; no
  API key and no OAuth2 token can perform it, whatever scopes it holds.
- `OPERATION_REQUIRES_API_KEY` — the reverse: the operation needs a scope that only an API
  key can hold (such as `accounts:write`), so a dashboard session never satisfies it.
- `API_KEY_LIVE_NOT_AVAILABLE` — the call came with a live API key (`beel_sk_live_…`) and
  the account has no Live mode yet. It is never degraded to Test: use your test key, or
  activate Live first.
- `ACCOUNT_REQUIRES_FOCUS_SWITCH` — dashboard session only: you are a member of the account
  in the path, but it is not the account in focus. Switch focus to it and retry.
- `REQUEST_BLOCKED` — the edge security filter stopped the request before the API saw its
  body, usually because a text field carries something that looks like markup or a
  script (HTML tags, `<script>`). Review the body 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": "FORBIDDEN",
    "message": "You do not have permission to access this resource"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

#### 404: The resource addressed by the path does not exist, or is not one this credential can see —
the two are answered identically, so existence is never disclosed. `error.code` names the
kind of resource that was missing, so a client can tell which of several ids in a path
failed to resolve:

- `INVOICE_NOT_FOUND` — the `{invoice_id}` (invoices, proformas and their sub-resources).
- `RECURRING_NOT_FOUND` — the `{recurring_invoice_id}`.
- `CLIENT_NOT_FOUND` — the `{customer_id}`.
- `INVOICE_CLIENT_NOT_FOUND` — the `customer_id` referenced by an invoice body does not
  resolve to a customer of the issuing company.
- `PRODUCT_NOT_FOUND` — the `{product_id}`.
- `SERIES_NOT_FOUND` — the `{series_id}`.
- `WEBHOOK_SUBSCRIPTION_NOT_FOUND`, `DELIVERY_LOG_NOT_FOUND` — the `{webhook_id}` and the
  `{delivery_id}`.
- `PROFILE_NOT_FOUND` — the credential's account has no company profile yet: its holder
  has not completed onboarding, so there is no issuer to act as.
- `NOT_FOUND` — the generic fallback, for the few resources that carry no code of their own.

Some resources declare a more specific `404` response of their own (`CONNECTION_NOT_FOUND`,
`MEMBER_NOT_FOUND`, `GRANT_NOT_FOUND`, `INVITATION_NOT_FOUND`, `REQUEST_LOG_NOT_FOUND`); it
is documented on the operation.

A `404` with `ENDPOINT_NOT_FOUND` is a different answer: the **path itself** does not exist
in this API (a typo in the route, or a resource that was never here). It says nothing about
any resource. When the path exists but not with that method, the answer is `405`, not `404`.


**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": "INVOICE_NOT_FOUND",
    "message": "Invoice not found"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

#### 409: Duplicate customer (same tax identifier)

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

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