# List the tax types allowed in Spain API Reference

Returns the tax regimes and percentages that BeeL accepts on an invoice. Use it to
validate a rate before sending it, or to build your own picker instead of hard-coding the
percentages.

- **Contents:** VAT (mainland), IGIC (Canary Islands), IPSI (Ceuta and Melilla), the
  withholding (IRPF) percentages, the equivalence surcharge that corresponds to each VAT
  rate, and the exemption reasons with the classification each one implies.
- **Scope:** the catalogue is the same for every credential and does not depend on any
  account or on any NIF, so the operation takes no identifier and works before the first
  NIF exists.

## VAT rates and the zero case

- **VAT lists 2, 4, 5, 7.5, 10 and 21, and deliberately not 0:** under VAT (and IPSI) a 0 % is not
  a rate but the exemption/non-subject sentinel, and on its own it says nothing. A 0 %
  line is only valid together with an `exemption_reason`, which this same response
  publishes under `exemption_reasons`.
- **IGIC does list 0:** there it is the real "Tipo Cero" and needs no reason.
- **Temporary VAT rates carry their period:** 5 % (2022-07-01 to 2024-09-30), and 2 % and
  7.5 % (2024-10-01 to 2024-12-31) are listed with `valid_from` / `valid_until`. They no
  longer apply to new operations but stay listed, because correctives and late filings for
  those periods still need them; an invoice whose operation date falls outside the period
  is rejected with `422 VAT_RATE_NOT_ACCEPTED_ON_DATE`. Every other rate has both `null`.
- **Equivalence surcharges are listed per VAT ↔ surcharge pair**, each with its period.


## GET /v1/tax-types

**List the tax types allowed in Spain**

Returns the tax regimes and percentages that BeeL accepts on an invoice. Use it to
validate a rate before sending it, or to build your own picker instead of hard-coding the
percentages.

- **Contents:** VAT (mainland), IGIC (Canary Islands), IPSI (Ceuta and Melilla), the
  withholding (IRPF) percentages, the equivalence surcharge that corresponds to each VAT
  rate, and the exemption reasons with the classification each one implies.
- **Scope:** the catalogue is the same for every credential and does not depend on any
  account or on any NIF, so the operation takes no identifier and works before the first
  NIF exists.

## VAT rates and the zero case

- **VAT lists 2, 4, 5, 7.5, 10 and 21, and deliberately not 0:** under VAT (and IPSI) a 0 % is not
  a rate but the exemption/non-subject sentinel, and on its own it says nothing. A 0 %
  line is only valid together with an `exemption_reason`, which this same response
  publishes under `exemption_reasons`.
- **IGIC does list 0:** there it is the real "Tipo Cero" and needs no reason.
- **Temporary VAT rates carry their period:** 5 % (2022-07-01 to 2024-09-30), and 2 % and
  7.5 % (2024-10-01 to 2024-12-31) are listed with `valid_from` / `valid_until`. They no
  longer apply to new operations but stay listed, because correctives and late filings for
  those periods still need them; an invoice whose operation date falls outside the period
  is rejected with `422 VAT_RATE_NOT_ACCEPTED_ON_DATE`. Every other rate has both `null`.
- **Equivalence surcharges are listed per VAT ↔ surcharge pair**, each with its period.

### Authentication

Accepts any of:

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

### Responses

#### 200: Catalog retrieved 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** `TaxTypesCatalog`

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

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


**Content `application/json`:**

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

**Example:**

```json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The parameter 'invoice_id' has an invalid type. Expected: UUID.",
    "details": {
      "field": "invoice_id",
      "invalid_value": "deliveries",
      "expected_format": "UUID"
    }
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

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


**Content `application/json`:**

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

**Example:**

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

#### 403: Authenticated but not allowed. Ten causes, told apart by `error.code`. The list is
**closed**: every 403 this API returns carries one of these ten, so 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.


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

#### 429: Rate limit exceeded

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

## TaxTypesCatalogResponse

- **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** `TaxTypesCatalog`

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

## TaxTypesCatalog

- **tax_regimes** `array[TaxRegime]`: No description
- **irpf_types** `array[IrpfType]`: No description
- **equivalence_surcharges** `array[EquivalenceSurcharge]`: No description
- **exemption_reasons** `array[ExemptionReasonCatalogEntry]`: No description

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

## TaxRegime

- **code** (required) `string`: Tax regime code. - `IVA`: Value Added Tax (Iberian Peninsula and Balearic Islands). - `IGIC`: Canary Islands General Indirect Tax. - `IPSI`: Production, Services and Import Tax (Ceuta and Melilla). - `OTHER`: special regimes outside the three above. — one of: IVA, IGIC, IPSI, OTHER (example: "IVA")
- **name** (required) `string`: Tax regime name (example: "IVA")
- **description** (required) `string`: Detailed description of the tax regime (example: "Value Added Tax")
- **tax_rates** (required) `array[TaxPercentage]`: Available percentages in this regime
- **applies_equivalence_surcharge** (required) `boolean`: Whether this regime applies equivalence surcharge
- **regime_keys** (required) `array[VeriFactuRegimeKey]`: Valid VeriFactu regime keys for this tax type

## IrpfType

- **percentage** (required) `number`: IRPF withholding percentage (example: 15)
- **description** (required) `string`: IRPF type description (example: "IRPF Profesional (15%)")
- **active** (required) `boolean`: Whether the IRPF type is active

## EquivalenceSurcharge

One VAT ↔ surcharge pair AEAT accepts, plus the `0` entry for "no surcharge". The same
surcharge can appear twice with different VAT rates and periods (0.5 goes with 4 % always
and with 5 % up to 2022-12-31).

- **percentage** (required) `number`: Equivalence surcharge percentage (example: 5.2)
- **associated_vat** (required) `number`: VAT percentage to which this surcharge is associated (example: 21)
- **description** (required) `string`: Equivalence surcharge description (example: "RE 5.2% (IVA 21%)")
- **active** (required) `boolean`: Whether the surcharge is active
- **valid_from** `string` (date): First operation date on which AEAT accepts this VAT ↔ surcharge pair; `null` when it has no start. The date judged is the invoice's `operation_date`, or its issue date when it has none. (example: null)
- **valid_until** `string` (date): Last operation date on which AEAT accepts this VAT ↔ surcharge pair; `null` when it has no end. A rate with a `valid_until` in the past only fits invoices for operations of its period. (example: null)

## ExemptionReasonCatalogEntry

- **code** (required) `string`: Enum value to send in invoice line exemption_reason field (example: "EXENTA_ART_20")
- **label** (required) `string`: i18n key for the label (resolve via translations) (example: "invoice.exemption.EXENTA_ART_20")
- **description** (required) `string`: i18n key for the description (example: "invoice.exemption.EXENTA_ART_20")
- **category** (required) `string`: Category grouping for UI display (example: "OPERACIONES EXENTAS")
- **classification_type** (required) `string`: Tax classification type that determines VeriFactu behavior: - EXENTA: Exempt operation (E1-E6), no tax fields - NO_SUJETA: Not subject to tax (N1), no tax fields - NO_SUJETA_LOCALIZACION: Not subject by localization rules (N2), no tax fields - SUJETA_NO_EXENTA_ISP: Reverse charge (S2), tax rate forced to 0% - SUJETA_NO_EXENTA: Normal taxed (S1), requires tax rate — one of: EXENTA, NO_SUJETA, NO_SUJETA_LOCALIZACION, SUJETA_NO_EXENTA_ISP, SUJETA_NO_EXENTA
- **available_as_default** (required) `boolean`: Whether a NIF can declare this reason as its **default exemption** in `PUT /v1/configuration/taxes`, or only per invoice line. Only the reasons the issuer verifies on its own qualify (`EXENTA_ART_20` and `OTRO`). The ones depending on the recipient or the operation would assert a fact — printed as a legal mention on every PDF — that nobody checked, and the `REGIMEN_ART_*` ones are already derived from the stored regime key. (example: false)
- **available_on_invoice_line** (required) `boolean`: Whether this reason can actually be sent in an invoice line's `exemption_reason`. Some reasons are published but not accepted: they are special-regime codes that do not travel in VeriFactu's `operacion_exenta`, so the API rejects them on POST. They are still listed —**marked, not removed**— because this catalogue also translates codes that are already stored on past invoices. **Which ones is deliberately not written here.** Read this flag per entry instead of hard-coding a list: it is derived from the same VeriFactu catalogue that decides the rejection, so it follows AEAT the day the set changes — a list copied into prose (or into your code) would not. (example: true)

## TaxPercentage

- **percentage** (required) `number`: Tax percentage (example: 21)
- **description** (required) `string`: Tax type description (example: "IVA (21%)")
- **active** (required) `boolean`: Indicates whether the type is active
- **associated_equivalence_surcharge** `number`: Associated equivalence surcharge percentage (only for VAT) (example: 5.2)
- **valid_from** `string` (date): First operation date on which AEAT accepts this rate; `null` when it has no start. The date judged is the invoice's `operation_date`, or its issue date when it has none. (example: null)
- **valid_until** `string` (date): Last operation date on which AEAT accepts this rate; `null` when it has no end. A rate with a `valid_until` in the past only fits invoices for operations of its period. (example: null)

## VeriFactuRegimeKey

- **code** (required) `string`: VeriFactu regime code (example: "01")
- **description** (required) `string`: Regime key description (example: "General regime operation")


---

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