# Create a product for a company API Reference

Creates a product or service in the catalog of the company in the path and returns it with
its `id`. A product holds reusable defaults for invoice lines: name, price, unit and taxes.

- **Required:** only `name`. Omitted, `category` is `SERVICE`, and the product is created
  active.
- **`code`:** optional, and unique within the company. A code another product of the
  company already uses answers `409` with `PRODUCT_DUPLICATE`.
- **Taxes:** `equivalence_surcharge_rate` has to be coherent with `main_tax.regime_key`;
  an incoherent pair answers `422` (see the field).
- **Several at once:** `POST /v1/companies/{company_id}/products/bulk` creates up to 100
  in one call.


## POST /v1/companies/{company_id}/products

**Create a product for a company**

Creates a product or service in the catalog of the company in the path and returns it with
its `id`. A product holds reusable defaults for invoice lines: name, price, unit and taxes.

- **Required:** only `name`. Omitted, `category` is `SERVICE`, and the product is created
  active.
- **`code`:** optional, and unique within the company. A code another product of the
  company already uses answers `409` with `PRODUCT_DUPLICATE`.
- **Taxes:** `equivalence_surcharge_rate` has to be coherent with `main_tax.regime_key`;
  an incoherent pair answers `422` (see the field).
- **Several at once:** `POST /v1/companies/{company_id}/products/bulk` creates up to 100
  in one call.

### Authentication

Accepts any of:

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

### Parameters

- **company_id** (required) in path `string`: Unique identifier (UUID) of the company the operation acts on — its identifier, not its NIF. It is the only source of context: the account that owns it is derived from it, and the `BeeL-Active-Company` header plays no part. A company you do not reach answers `403`, and so does a company that does not exist, so the existence of a company in another account is never disclosed.
- **Idempotency-Key** (optional) in header `string`: Idempotency key to prevent duplicates in sensitive operations. - Any unique client-generated string (e.g. an order id). A UUID also works but is not required - Allowed characters: letters, digits, `_` and `-` (max 255 chars) - Retrying with the same key replays the first response when it was a success (2xx) or a server error (5xx): same status and body, plus the header `Idempotency-Replay: true`. After a 5xx, check whether the operation took effect before retrying with a **new** key - A 4xx is not stored: the key is released, so the corrected request can reuse it - Stored responses expire 24 hours after processing The key is scoped per user and environment, and bound to the request body, so retrying after a network timeout replays the stored response instead of repeating the operation. | Status | Code | When | |---|---|---| | `400` | `INVALID_IDEMPOTENCY_KEY` | The key breaks the format rules above. | | `409` | `IDEMPOTENCY_KEY_PROCESSING` | The first request is still in flight. Wait for the `Retry-After` seconds (2) and retry with the same key. | | `409` | `IDEMPOTENCY_KEY_MISMATCH` | The key was already used with a **different** body. Use a new key. |

### Request Body

Required.

**Content `application/json`:**

- **code** `string`: Unique alphanumeric product code (optional) (example: "SERV-001")
- **name** (required) `string`: Product/service name (example: "Technical consulting")
- **description** `string`: Detailed description (optional) (example: "Specialized technical consulting services")
- **category** `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** `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). Must be coherent with `main_tax.regime_key`: a surcharge > 0 is only valid under regime `18`. When `regime_key` is omitted it is derived automatically (`18` with a surcharge > 0, `01` otherwise). An explicit `regime_key` that does not admit a surcharge combined with a surcharge > 0 is rejected with a 422 (`SURCHARGE_REQUIRES_REGIME`), and an explicit regime `18` without a surcharge > 0 is rejected with a 422 (`REGIME_REQUIRES_SURCHARGE`). (example: 5.2)
- **irpf_rate** `number`: IRPF withholding percentage (optional). 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)

**Example `physical_product`** — Physical product:

```json
{
  "code": "PROD-LAPTOP-001",
  "name": "Ordenador portátil HP ProBook 450 G10",
  "description": "Incluye garantía de 2 años",
  "category": "PRODUCT",
  "default_price": 850,
  "unit": "unidad",
  "main_tax": {
    "type": "IVA",
    "percentage": 21,
    "regime_key": "01"
  }
}
```

**Example `service_product`** — Service product:

```json
{
  "code": "SRV-001",
  "name": "Consultoría estratégica",
  "description": "Servicios de consultoría estratégica por hora",
  "category": "CONSULTING",
  "default_price": 125,
  "unit": "hora",
  "main_tax": {
    "type": "IVA",
    "percentage": 21,
    "regime_key": "01"
  }
}
```

### Responses

#### 201: Product created successfully

**Headers:**

- `Location` `string`: URI of the created resource — its canonical GET (`/v1/companies/{company_id}/...` or `/v1/accounts/{account_id}/...`).

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

**Example `product_created_success`** — Product created (201):

```json
{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "code": "SRV-001",
    "name": "Consultoría estratégica",
    "description": "Servicios de consultoría estratégica por hora",
    "category": "CONSULTING",
    "default_price": 125,
    "unit": "hora",
    "main_tax": {
      "type": "IVA",
      "percentage": 21,
      "regime_key": "01"
    },
    "active": true,
    "created_at": "2025-02-09T10:00:00Z",
    "updated_at": "2025-02-09T10:00:00Z"
  },
  "meta": {
    "timestamp": "2025-02-09T10:00:00Z",
    "request_id": "req_producto_001"
  }
}
```

#### 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: Your account does not own or manage this company, or does not hold the required
access over it. A NIF that does not exist answers the same way.
That case answers `error.code` `ACTIVE_COMPANY_NOT_ACCESSIBLE`.


**Content `application/json`:**

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

#### 409: Duplicate code or request already processed

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

#### 429: Rate limit exceeded. `RATE_LIMIT_EXCEEDED` when the request rate of your credential is
over its limit; `TOO_MANY_REQUESTS` when an email the operation sends is held back because
too many went out to that recipient in a short time. Either way, retry after the
`Retry-After` seconds.

`QUOTA_EXCEEDED` is the other `429`: the operation would go over a quota of your plan
for the current period (issued invoices, for instance). Retrying does not help until the
quota resets or the plan changes. `error.details` carries `feature_code`,
`current_usage`, `quota_limit` and `reset_date`, and `RateLimit-Reset` the seconds until
the reset when it is known.


**Headers:**

- `Retry-After` `integer`: Seconds until the rate limit resets
- `RateLimit-Limit` `integer`: Maximum requests allowed in the window
- `RateLimit-Remaining` `integer`: Remaining requests in the current window
- `RateLimit-Reset` `integer`: Seconds until the current window resets

**Content `application/json`:**

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

**Example:**

```json
{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests. Please try again in 60 seconds."
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

#### 500: Internal server error

**Content `application/json`:**

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

**Example:**

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

#### default: Any status code the operation does not list above. Every operation declares it, so a
generated client always has a branch to fall into and never loses the cause of a failure
it did not anticipate.

This is where the transport-level answers land — `405`, `406`, `415` and `429` — together
with any status a future version of the API starts returning. All of them carry the same
error envelope as the codes listed explicitly, so `error.code` is what tells them apart:
switching on the status code alone is not enough. See «Transport-level errors» in the
API description for when each one is produced.

A `402` carrying `SUBSCRIPTION_REQUIRED` lands here when the account has no active
subscription. `error.details` says why and what to do: `current_status` (absent if it
never had one), `required_permission` and `action`.

A `503` carrying `SERVICE_UNAVAILABLE` lands here too, with a `Retry-After` header: a
dependency of BeeL itself (the database, the email provider) was momentarily
unavailable and nothing was done. Retry after those seconds.

A `502` carrying `EXTERNAL_SERVICE_ERROR` also lands here: an outbound integration the
operation depends on failed or did not answer in time. It is a transient condition — retry
with the same `Idempotency-Key` where the operation accepts one.

One exception to the envelope: a failure of the network edge that never reaches the
application (`502`, `503`, `504`, `524`) is generated by Cloudflare and its body is not
BeeL's — it may not even be JSON. Treat those as "no answer", and retry.


**Content `application/json`:**

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

**Example:**

```json
{
  "success": false,
  "error": {
    "code": "UNSUPPORTED_MEDIA_TYPE",
    "message": "Unsupported media type: text/plain. Supported: application/json"
  },
  "meta": {
    "timestamp": "2025-01-15T10:30:00Z",
    "request_id": "4bf92f3577b34da6a3ce929d0e0e4736"
  }
}
```

---

# Related Schema Definitions

## CreateProductRequest

- **code** `string`: Unique alphanumeric product code (optional) (example: "SERV-001")
- **name** (required) `string`: Product/service name (example: "Technical consulting")
- **description** `string`: Detailed description (optional) (example: "Specialized technical consulting services")
- **category** `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** `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). Must be coherent with `main_tax.regime_key`: a surcharge > 0 is only valid under regime `18`. When `regime_key` is omitted it is derived automatically (`18` with a surcharge > 0, `01` otherwise). An explicit `regime_key` that does not admit a surcharge combined with a surcharge > 0 is rejected with a 422 (`SURCHARGE_REQUIRES_REGIME`), and an explicit regime `18` without a surcharge > 0 is rejected with a 422 (`REGIME_REQUIRES_SURCHARGE`). (example: 5.2)
- **irpf_rate** `number`: IRPF withholding percentage (optional). 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)

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

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

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