# Update a product partially (deprecated) API Reference

Updates only the fields present in the body, leaving every other field of the product as it
is — in particular `main_tax`, `irpf_rate` and `equivalence_surcharge_rate`, which `PUT`
resets.

- **Null vs omitted:** a field sent as `null` is cleared, which is different from omitting
  it (see `PatchProductRequest`). The result goes through the same validation as `PUT`.
- **Deprecated:** use `PATCH /v1/companies/{company_id}/products/{product_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/products/{product_id}

**Update a product partially**

Updates only the fields present in the body, leaving every other field of the product as it
is — in particular `main_tax`, `irpf_rate` and `equivalence_surcharge_rate`, which `PUT`
resets.

- **Null vs omitted:** a field sent as `null` is cleared, which is different from omitting
  it (see `PatchProductRequest`). The result goes through the same validation as `PUT`.
- **Deprecated:** use `PATCH /v1/companies/{company_id}/products/{product_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

- **product_id** (required) in path `string`: Product unique UUID

### Request Body

Required.

**Content `application/json`:**

- **code** `string`: Unique alphanumeric product code. Send `null` to clear it. (example: "SERV-001")
- **name** `string`: Product/service name. Cannot be cleared. (example: "Technical consulting")
- **description** `string`: Detailed description. Send `null` to clear it. (example: "Specialized technical consulting services")
- **category**: Product category. Omit to keep the current one.
- **default_price** `number`: Suggested default price. Send `null` to clear it. (example: 85.5)
- **unit** `string`: Unit of measure. Send `null` to clear it. (example: "hours")
- **main_tax**: Main tax. Replaced as a whole (it is not patched field by field); omit it to keep the current one. A product always carries a main tax, so it cannot be cleared.
- **equivalence_surcharge_rate** `number`: Equivalence surcharge percentage. Send `null` to state that none applies (equivalent to `0`). The merged result must be coherent with the regime key: if this PATCH does not send `main_tax.regime_key`, the stored regime is adjusted automatically (`18` when the merged surcharge is > 0, `01` when it is not). With an explicit `regime_key` in this PATCH, an incoherent combination is rejected with a 422 (`SURCHARGE_REQUIRES_REGIME` / `REGIME_REQUIRES_SURCHARGE`). (example: 5.2)
- **irpf_rate** `number`: IRPF withholding percentage. Send `null` to state that none applies (equivalent to `0`). The rate must be one of `IrpfPercentage` (`INVALID_IRPF` otherwise) and one the company can bear, the same check as an invoice line (see `WithholdingOptions` in the tax configuration): an entity gets the rates of individuals rejected with `IRPF_RATE_NOT_FOR_CORPORATE_ISSUER`, and an individual gets `9.5` rejected with `IRPF_RATE_ONLY_FOR_CORPORATE_ISSUER`. (example: 15)
- **active** `boolean`: Indicates whether the product is active. (example: true)

### Responses

#### 200: Product 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** `Product`

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

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


**Content `application/json`:**

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

**Example:**

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

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


**Content `application/json`:**

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

**Example:**

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

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

**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: Unprocessable entity - validation or business rule error

Other `error.code` values with this status:

- `INVALID_IRPF` — the `irpf_rate` is not a withholding rate in force (see `IrpfPercentage`); send one of the published rates.
- `INVALID_PERCENTAGE` — the `percentage` is not a rate of that tax type (e.g. IVA 13); the message lists the valid rates. OSS key `17` accepts any EU rate.
- `INVALID_REGIME_KEY_FOR_TAX_TYPE` — the `regime_key` does not exist for that tax type (e.g. `18` under IGIC); `details.allowed_values` lists the accepted keys.
- `INVALID_SURCHARGE` — the `equivalence_surcharge_rate` is not an equivalence surcharge rate (see `EquivalenceSurchargePercentage`); send one of the published values.
- `IRPF_RATE_NOT_FOR_CORPORATE_ISSUER` — the issuer is a company (corporate income tax), which can only withhold 0, 19 or 24 %; use one of those.
- `IRPF_RATE_ONLY_FOR_CORPORATE_ISSUER` — the rate (e.g. 9.5 % Ceuta/Melilla) only applies to corporate issuers; an individual issuer must use a personal-income-tax rate.
- `REGIME_REQUIRES_SURCHARGE` — a product with explicit `regime_key: "18"` has no `equivalence_surcharge_rate` > 0; add the surcharge or omit the key.
- `SURCHARGE_REQUIRES_REGIME` — an equivalence surcharge was sent with a `regime_key` other than `01`/`18` (e.g. `02`, `03`, `17`); only regime `18` admits it.


**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": "UNPROCESSABLE_ENTITY",
    "message": "Data cannot be processed",
    "details": {
      "field": "Specific error description"
    }
  },
  "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

## PatchProductRequest

Partial update of a product (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 product goes through the same validation as `PUT`
(duplicate code check excluding this product, field formats, tax ranges).

- **code** `string`: Unique alphanumeric product code. Send `null` to clear it. (example: "SERV-001")
- **name** `string`: Product/service name. Cannot be cleared. (example: "Technical consulting")
- **description** `string`: Detailed description. Send `null` to clear it. (example: "Specialized technical consulting services")
- **category**: Product category. Omit to keep the current one.
- **default_price** `number`: Suggested default price. Send `null` to clear it. (example: 85.5)
- **unit** `string`: Unit of measure. Send `null` to clear it. (example: "hours")
- **main_tax**: Main tax. Replaced as a whole (it is not patched field by field); omit it to keep the current one. A product always carries a main tax, so it cannot be cleared.
- **equivalence_surcharge_rate** `number`: Equivalence surcharge percentage. Send `null` to state that none applies (equivalent to `0`). The merged result must be coherent with the regime key: if this PATCH does not send `main_tax.regime_key`, the stored regime is adjusted automatically (`18` when the merged surcharge is > 0, `01` when it is not). With an explicit `regime_key` in this PATCH, an incoherent combination is rejected with a 422 (`SURCHARGE_REQUIRES_REGIME` / `REGIME_REQUIRES_SURCHARGE`). (example: 5.2)
- **irpf_rate** `number`: IRPF withholding percentage. Send `null` to state that none applies (equivalent to `0`). The rate must be one of `IrpfPercentage` (`INVALID_IRPF` otherwise) and one the company can bear, the same check as an invoice line (see `WithholdingOptions` in the tax configuration): an entity gets the rates of individuals rejected with `IRPF_RATE_NOT_FOR_CORPORATE_ISSUER`, and an individual gets `9.5` rejected with `IRPF_RATE_ONLY_FOR_CORPORATE_ISSUER`. (example: 15)
- **active** `boolean`: Indicates whether the product is active. (example: true)

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

## Product

- **id** (required) `string` (uuid): Unique UUID of the product/service (example: "550e8400-e29b-41d4-a716-446655440000")
- **code** `string`: Unique alphanumeric product code (optional) (example: "SERV-001")
- **name** (required) `string`: Product/service name (example: "Technical consulting")
- **description** `string`: Detailed product/service description (optional) (example: "Specialized technical consulting services in web development")
- **category** (required) `ProductCategory`: Product/service category: * PRODUCT - Physical, tangible products * SERVICE - General services * CONSULTING - Consulting and advisory services * SOFTWARE - Development, licenses, SaaS * TRAINING - Courses, workshops, training * OTHER - Other unclassified types
- **default_price** `number`: Suggested default price (optional) (example: 85.5)
- **unit** `string`: Unit of measure (optional) (example: "hours")
- **main_tax** (required) `TaxInfo`: Complete tax information with cross-validations: - IVA: real rates 4, 10, 21, and the temporary 2, 5 and 7.5 (see below for 0) - IGIC: 0, 3, 5, 7, 9.5, 15, 20 — here 0 is the real "Tipo Cero" - IPSI: real rates 0.5, 1, 2, 4, 8, 10 (see below for 0) - OTHER: any percentage between 0 and 100 **0 % under IVA and IPSI is not a rate, it is the exemption sentinel.** It is accepted on a line, but only together with an `exemption_reason` (exempt or non-subject operation); on its own it says nothing and the line is rejected. That is why `GET /v1/tax-types` publishes the IVA rates without 0: the legitimate way to a 0 % IVA line is through an exemption reason, which the same response also publishes. IGIC is different — its 0 % is a real legal rate (basic necessities) and needs no reason. **IVA 5 %** (the temporary rate applied from 2022 to electricity, gas and certain foodstuffs) is no longer in force for new operations. AEAT only accepts it on operations dated from 2022-07-01 to 2024-09-30: send the `operation_date` of that period, because without one the issue date decides and a line at 5 % is rejected with `422 VAT_RATE_NOT_ACCEPTED_ON_DATE`. Its equivalence surcharge pair is 0.5 up to 2022-12-31 and 0.62 from 2023-01-01. **IVA 2 % and 7.5 %** (temporary rates of the last quarter of 2024) are accepted only on operations dated from 2024-10-01 to 2024-12-31, with surcharges 0.26 and 1. Exception: when regime_key = "17" (OSS/IOSS) the invoice applies the destination country VAT instead of the Spanish one, so any percentage in the EU range [0, 27] is accepted regardless of the tax type set — including 0 without an exemption reason.
- **equivalence_surcharge_rate** `number`: Equivalence surcharge percentage (optional) (example: 5.2)
- **irpf_rate** `number`: IRPF withholding percentage (optional) (example: 15)
- **active** (required) `boolean`: Indicates whether the product is active (example: true)
- **created_at** (required) `string` (date-time): Creation date and time (example: "2025-01-18T10:30:00Z")
- **updated_at** (required) `string` (date-time): Last update date and time (example: "2025-01-18T15:45:30Z")

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

## ProductCategory

Product/service category:
* PRODUCT - Physical, tangible products
* SERVICE - General services
* CONSULTING - Consulting and advisory services
* SOFTWARE - Development, licenses, SaaS
* TRAINING - Courses, workshops, training
* OTHER - Other unclassified types

Type: `string` — one of: PRODUCT, SERVICE, CONSULTING, SOFTWARE, TRAINING, OTHER

## TaxInfo

Complete tax information with cross-validations:
- IVA: real rates 4, 10, 21, and the temporary 2, 5 and 7.5 (see below for 0)
- IGIC: 0, 3, 5, 7, 9.5, 15, 20 — here 0 is the real "Tipo Cero"
- IPSI: real rates 0.5, 1, 2, 4, 8, 10 (see below for 0)
- OTHER: any percentage between 0 and 100

**0 % under IVA and IPSI is not a rate, it is the exemption sentinel.** It is accepted
on a line, but only together with an `exemption_reason` (exempt or non-subject
operation); on its own it says nothing and the line is rejected. That is why
`GET /v1/tax-types` publishes the IVA rates without 0: the legitimate way
to a 0 % IVA line is through an exemption reason, which the same response also
publishes. IGIC is different — its 0 % is a real legal rate (basic necessities) and
needs no reason.

**IVA 5 %** (the temporary rate applied from 2022 to electricity, gas and certain
foodstuffs) is no longer in force for new operations. AEAT only accepts it on operations
dated from 2022-07-01 to 2024-09-30: send the `operation_date` of that period, because
without one the issue date decides and a line at 5 % is rejected with
`422 VAT_RATE_NOT_ACCEPTED_ON_DATE`. Its equivalence surcharge pair is 0.5 up to 2022-12-31
and 0.62 from 2023-01-01. **IVA 2 % and 7.5 %** (temporary rates of the last quarter of 2024)
are accepted only on operations dated from 2024-10-01 to 2024-12-31, with surcharges 0.26
and 1.

Exception: when regime_key = "17" (OSS/IOSS) the invoice applies the destination
country VAT instead of the Spanish one, so any percentage in the EU range [0, 27]
is accepted regardless of the tax type set — including 0 without an exemption reason.

- **type** (required) `TaxType`: Tax type by territory: - IVA: Iberian Peninsula and Balearic Islands (4%, 10%, 21%; 2%, 5% and 7.5% only on operations of their period) - IGIC: Canary Islands (0%, 3%, 5%, 7%, 9.5%, 15%, 20%) - IPSI: Ceuta and Melilla (0.5%, 1%, 2%, 4%, 8%, 10%) - OTHER: Configurable 0%-100% Under IVA and IPSI, 0% is not one of these rates: it is the exemption/non-subject sentinel and always travels with an `exemption_reason`. IGIC's 0% is a real rate. See `TaxInfo` for the full rules.
- **percentage** (required) `number`: Tax percentage (example: 21)
- **regime_key** `RegimeKey`: Regime key according to VeriFactu regulations. Omitted, `01` (general regime) applies: - 01: General regime operation - 02: Export (IVA and IGIC; not IPSI, whose AEAT list is `01, 08, 11, 18, 19, 20`) - 03: Used goods, art, antiques (not accepted, see below) - 04: Investment gold - 05: Travel agencies - 06: Group of entities (not accepted, see below) - 07: Cash basis - 08: Operation subject to another indirect tax — IPSI or IGIC on an IVA line, IPSI or IVA on an IGIC line. It is **not** the general regime of IGIC, which is `01`. - 09: Mediating agencies - 10: Third-party collections - 11: Local rental - 14: VAT pending in certifications (not accepted, see below) - 15: VAT pending successive tract - 17: OSS and IOSS - 18: Equivalence surcharge - 19: REAGYP - 20: Simplified regime **What AEAT requires with each key** (Validaciones VERI*FACTU 3.1.3.15.6), checked on IVA and IGIC lines before the invoice is numbered. Otherwise the request is rejected with `422` and the code in brackets: - `04`: only reverse charge (an `ISP_ART_84_2_*` reason) or an exemption (`REGIME_KEY_CLASSIFICATION_NOT_ACCEPTED`). - `08`: only `exemption_reason: NO_SUJETA_LOCALIZACION`, at 0 % (`REGIME_KEY_CLASSIFICATION_NOT_ACCEPTED`). - `10`: only `exemption_reason: NO_SUJETA_ART_7_9`, on a `STANDARD` invoice whose recipient has a `nif` (`REGIME_KEY_CLASSIFICATION_NOT_ACCEPTED`, `REGIME_KEY_REQUIRES_STANDARD_INVOICE`, `REGIME_KEY_REQUIRES_RECIPIENT_NIF`). - `11` (IVA): a subject line only at 21 %, and no reverse charge (`REGIME_KEY_REQUIRES_VAT_RATE`, `REGIME_KEY_CLASSIFICATION_NOT_ACCEPTED`). - `06` and `14` are not accepted (`REGIME_KEY_NOT_SUPPORTED`): AEAT requires with them data the invoice does not carry (a cost-based taxable base; an operation date after the issue date and a public-administration recipient). - `03` (used goods) is not accepted (`REGIME_KEY_NOT_SUPPORTED`): under it the invoice must not show the tax separately (RD 1619/2012, art. 16.2.c), and it always does. The corrective of an invoice that already carried `03` keeps it. - `05` (travel agencies) and `07` (cash basis) are accepted, and the invoice PDF carries the mention of their regime (RD 1619/2012, art. 6.1 n and p). `07`: no reverse charge, no non-subject reason and, of the exemptions, only art. 20 or `OTRO` (`REGIME_KEY_CLASSIFICATION_NOT_ACCEPTED`). `GET /v1/tax-types` only offers the keys that are accepted. **One exception to "a key you send is the key you get":** when the line ends up carrying an equivalence surcharge — whether you sent `equivalence_surcharge_rate` or it was inherited from the company's tax configuration — a `01` is rewritten to `18`, because a surcharge under the general regime is fiscally incoherent. Send `equivalence_surcharge_rate: 0` explicitly to keep `01`. See `equivalence_surcharge_rate` in the invoice line for the full rules.

## TaxType

Tax type by territory:
- IVA: Iberian Peninsula and Balearic Islands (4%, 10%, 21%; 2%, 5% and 7.5% only on operations of their period)
- IGIC: Canary Islands (0%, 3%, 5%, 7%, 9.5%, 15%, 20%)
- IPSI: Ceuta and Melilla (0.5%, 1%, 2%, 4%, 8%, 10%)
- OTHER: Configurable 0%-100%

Under IVA and IPSI, 0% is not one of these rates: it is the exemption/non-subject
sentinel and always travels with an `exemption_reason`. IGIC's 0% is a real rate.
See `TaxInfo` for the full rules.

Type: `string` — one of: IVA, IGIC, IPSI, OTHER

## RegimeKey

Regime key according to VeriFactu regulations. Omitted, `01` (general regime) applies:
- 01: General regime operation
- 02: Export (IVA and IGIC; not IPSI, whose AEAT list is `01, 08, 11, 18, 19, 20`)
- 03: Used goods, art, antiques (not accepted, see below)
- 04: Investment gold
- 05: Travel agencies
- 06: Group of entities (not accepted, see below)
- 07: Cash basis
- 08: Operation subject to another indirect tax — IPSI or IGIC on an IVA line, IPSI or IVA
  on an IGIC line. It is **not** the general regime of IGIC, which is `01`.
- 09: Mediating agencies
- 10: Third-party collections
- 11: Local rental
- 14: VAT pending in certifications (not accepted, see below)
- 15: VAT pending successive tract
- 17: OSS and IOSS
- 18: Equivalence surcharge
- 19: REAGYP
- 20: Simplified regime

**What AEAT requires with each key** (Validaciones VERI*FACTU 3.1.3.15.6), checked on
IVA and IGIC lines before the invoice is numbered. Otherwise the request is rejected with
`422` and the code in brackets:
- `04`: only reverse charge (an `ISP_ART_84_2_*` reason) or an exemption
  (`REGIME_KEY_CLASSIFICATION_NOT_ACCEPTED`).
- `08`: only `exemption_reason: NO_SUJETA_LOCALIZACION`, at 0 %
  (`REGIME_KEY_CLASSIFICATION_NOT_ACCEPTED`).
- `10`: only `exemption_reason: NO_SUJETA_ART_7_9`, on a `STANDARD` invoice whose
  recipient has a `nif` (`REGIME_KEY_CLASSIFICATION_NOT_ACCEPTED`,
  `REGIME_KEY_REQUIRES_STANDARD_INVOICE`, `REGIME_KEY_REQUIRES_RECIPIENT_NIF`).
- `11` (IVA): a subject line only at 21 %, and no reverse charge
  (`REGIME_KEY_REQUIRES_VAT_RATE`, `REGIME_KEY_CLASSIFICATION_NOT_ACCEPTED`).
- `06` and `14` are not accepted (`REGIME_KEY_NOT_SUPPORTED`): AEAT requires with them
  data the invoice does not carry (a cost-based taxable base; an operation date after the
  issue date and a public-administration recipient).
- `03` (used goods) is not accepted (`REGIME_KEY_NOT_SUPPORTED`): under it the invoice
  must not show the tax separately (RD 1619/2012, art. 16.2.c), and it always does. The
  corrective of an invoice that already carried `03` keeps it.
- `05` (travel agencies) and `07` (cash basis) are accepted, and the invoice PDF carries
  the mention of their regime (RD 1619/2012, art. 6.1 n and p). `07`: no reverse charge,
  no non-subject reason and, of the exemptions, only art. 20 or `OTRO`
  (`REGIME_KEY_CLASSIFICATION_NOT_ACCEPTED`).
`GET /v1/tax-types` only offers the keys that are accepted.

**One exception to "a key you send is the key you get":** when the line ends up
carrying an equivalence surcharge — whether you sent `equivalence_surcharge_rate`
or it was inherited from the company's tax configuration — a `01` is rewritten to
`18`, because a surcharge under the general regime is fiscally incoherent. Send
`equivalence_surcharge_rate: 0` explicitly to keep `01`. See
`equivalence_surcharge_rate` in the invoice line for the full rules.

Type: `string` — one of: 01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 14, 15, 17, 18, 19, 20


---

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