---
title: "List the products of a company"
method: GET
path: "/v1/companies/{company_id}/products"
tags: ["CompanyProducts"]
---

# List the products of a company

`GET /v1/companies/{company_id}/products`

## Request

- Base URL: `https://app.beel.es/api`
- URL: `https://app.beel.es/api/v1/companies/{company_id}/products`
- Auth: HTTP bearer

Returns a paginated list of the products/services of this company, with optional
filters.

- **`q`:** searching is done on this collection, there is no separate search path. `q`
  matches the name, the code and the description, so it returns at least everything the
  withdrawn `GET /v1/products/search` returned, in the paginated envelope of this list.

## Query parameters

- `page` integer
- `limit` integer
- `q` string
- `category` 'PRODUCT' | 'SERVICE' | 'CONSULTING' | 'SOFTWARE' | 'TRAINING' | 'OTHER': 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
- `active` boolean
- `name` string
- `code` string
- `min_price` number
- `max_price` number
- `sort_by` 'name' | 'code' | 'category' | 'default_price' | 'created_at': Product field the list is ordered by. - `name`: product name. - `code`: product code. - `category`: product category. - `default_price`: default unit price. - `created_at`: creation instant.
- `sort_order` 'asc' | 'desc': Sort direction. Shared vocabulary for every `sort_order` query param: declared once so the generator emits a real enum and an unknown direction is rejected with `400` instead of being silently ignored.

## Response `200`

Product list retrieved successfully

- object: 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` boolean, required
  - `data` object, 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.
    - `products` Product[], required
      - `id` string, uuid, required: Unique UUID of the product/service
      - `code` string, nullable: Unique alphanumeric product code (optional)
      - `name` string, required: Product/service name
      - `description` string: Detailed product/service description (optional)
      - `category` 'PRODUCT' | 'SERVICE' | 'CONSULTING' | 'SOFTWARE' | 'TRAINING' | 'OTHER', required: 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)
      - `unit` string: Unit of measure (optional)
      - `main_tax` TaxInfo, required: 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` 'IVA' | 'IGIC' | 'IPSI' | 'OTHER', required: 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` number, required: Tax percentage
        - `regime_key` '01' | '02' | '03' | '04' | '05' | '06' | '07' | '08' | '09' | '10' | '11' | '14' | '15' | '17' | '18' | '19' | '20': 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.
      - `equivalence_surcharge_rate` number: Equivalence surcharge percentage (optional)
      - `irpf_rate` number: IRPF withholding percentage (optional)
      - `active` boolean, required: Indicates whether the product is active
      - `created_at` string, date-time, required: Creation date and time
      - `updated_at` string, date-time, required: Last update date and time
    - `pagination` Pagination, required
      - `current_page` integer, required
      - `total_pages` integer, required: How many pages the query has. An empty collection has **one** page — the first one, empty — so the smallest value a canonical route returns is `1`. The deprecated flat aliases (`/v1/invoices`, `/v1/customers`, …) answer `0` for that same empty collection, and `has_next`/`has_previous` follow suit. They were left as they were on purpose: a client that reads `total_pages == 0` as "no results" would break if it changed under it. Do not compare the two families of routes field by field; read `total_items` if what you want to know is whether anything came back.
      - `total_items` integer, required
      - `items_per_page` integer, required
      - `has_next` boolean
      - `has_previous` boolean
  - `meta` ResponseMeta
    - `timestamp` string, date-time
    - `request_id` string

## Other responses

- `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.
- `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.
- `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`.
- `422`: A parameter parsed as its declared type but its value is not one the operation admits: a `limit` outside its range (`limit=0`, `limit=101`), a `page` under 1, a `cursor` that is not one this API issued, more keys than the `metadata` filter accepts, a value outside the supported set. The query was rejected whole and nothing was read. Where a `400` says the target could not be understood, this says it was understood and refused. Fix the value — retrying the same query answers the same way.
- `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.
- `500`: Internal server error
- `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.

---

[API](https://skmtc.dev/beel/apis/beel-public-api.md) · [All operations](https://skmtc.dev/beel/apis/beel-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/beel/apis/beel-public-api/revisions/ffbc0ae31f79?raw)
