# Resources under the NIF

An account can now hold several NIFs, each named in the path. The old flat routes are deprecated with a Sunset date and a successor — here's what moved where, when the old routes retire, and how to tell whether it affects you.

**What's new: an account can now hold several NIFs.** A fiscal identity is no
longer inferred from your credential — you name it in the path, so one account
can invoice under many NIFs. That's the model documented in
[Multi-NIF](/multi-nif) and [Companies](/multi-nif/companies); this note is the
API side of it.

Each resource now lives under the axis it belongs to: business resources under
the NIF (`/v1/companies/{company_id}`), account-wide ones under the account
(`/v1/accounts/{account_id}`), the shared catalogues at the top level, and your
preferred language on `/v1/me`. The full move is in the
[route table](#find-your-route) below.

**Nothing breaks today.** The old flat routes are deprecated, not removed: each
keeps answering and now carries `Deprecation: true`, a `Sunset` date and, where
one is declared, a `Link` to its successor. Four routes deliberately have none
— the `PUT`s that *replace* a customer, a product, a recurring invoice or a
series — and
the contract says so instead of pointing you at something that behaves
differently. They all retire on **9 December 2026** — migrate on your own
schedule until then.

<Callout type="info">
  **Want the exact list for your code?** If you use [Claude Code](https://claude.com/claude-code),
  the `beel-api` plugin has a skill (`/beel-api:upgrade`) that reads this note and
  your repository and comes back with the calls you need to change — jump to
  [Hand it to Claude Code](#hand-it-to-claude-code).
</Callout>

## What breaks

Five changes need more than a new path — a mechanical find-and-replace gets each
of them wrong:

- **`GET /v1/products/search` is the one trap.** Its successor is
  `GET /v1/companies/{company_id}/products`, which takes the same query filter
  but answers a different shape: the old route returned `data: [ … ]`, the
  successor returns `data: { products, pagination }`. Swapping the path without
  reading the schema leaves you mapping over an object.
- **The `PUT`s that really replace have no successor, on purpose.**
  `PUT /v1/customers/{customer_id}`, `PUT /v1/products/{product_id}`,
  `PUT /v1/recurring-invoices/{recurring_invoice_id}` and
  `PUT /v1/series/{series_id}` replace the whole
  resource: a field you leave out is cleared. The canonical form has a single
  update verb, `PATCH`, and it *merges* — so it is not a drop-in destination.
  The contract marks these four `x-no-successor`, and the API sends them no
  `Link: rel="successor-version"`, precisely so that nobody migrates them
  blind. Moving one of them to the company-scoped `PATCH` changes what your
  call does, from replacing to merging: go through those call sites by hand,
  and where you relied on replacement send the resource complete, passing
  `null` in every field you want cleared. Their `PATCH` siblings — which
  already existed on the flat routes — are the ones with a clean 1:1
  successor.
- **`PUT /v1/invoices/{invoice_id}` is the exception, and it costs you
  nothing.** It is the only `PUT` here with a declared successor,
  `PATCH /v1/companies/{company_id}/invoices/{invoice_id}`, and it earns it:
  this route never replaced the invoice. It applied only the fields present in
  the body and left the rest as they were — exactly what the successor does.
  The verb was the lie, not the behaviour. Change route and verb, keep the
  body, and the result is the same as before.
- **Action verbs converged onto `status`.** `mark-paid`, `mark-sent` and
  `revert-to-issued` all become
  `PUT /v1/companies/{company_id}/invoices/{invoice_id}/status` with the target
  status in the body; `pause` and `resume` do the same for recurring invoices.
  The old path is gone from the successor — the state you want is now data, not
  a URL.
- **The four `/v1/invoices/bulk/*` routes split by intent** into `pdf-archive`,
  `deliveries`, `batches` and `exports`, each with its own request and response.
  There is no single successor to find-and-replace them onto.

## Does this affect you?

- Grep your integration for `/v1/invoices`, `/v1/customers`, `/v1/products`,
  `/v1/recurring-invoices`, `/v1/configuration/`, `/v1/webhooks`, `/v1/emails`
  and `/v1/developers/request-logs`. Every hit is a deprecated route, and the
  table below gives its successor — or says it has none.
- Watch your responses: a deprecated route answers with `Deprecation: true`, a
  `Sunset` date and, when a successor is declared,
  `Link: <successor>; rel="successor-version"`. Log those headers for a week and
  you have the exact list of calls left to migrate, with no guessing — and a
  deprecated route arriving *without* the `Link` is one of the four replacing
  `PUT`s, which you have to move by hand.
- Check whether you send the `BeeL-Active-Company` header. Only the flat routes
  read it; once the NIF is in the path, the header plays no part.

## The retirement calendar

The deprecated routes retire on **9 December 2026**. One speed, with a grace period on every route until then.

| Phase | What happens |
|---|---|
| **Today** | Every successor route is live, and every deprecated route keeps answering exactly as before. Nothing 404s; nothing has been withdrawn. |
| **Grace period** | The 91 deprecated operations keep working, adding `Deprecation: true`, the `Sunset` date and — for all but the four replacing `PUT`s — a `Link` to their successor. You migrate on your own schedule while this lasts. |
| **9 December 2026** | The deprecated routes stop answering and return `410 Gone`. Every response until then carried the same `Sunset` date in its headers, so you can read it straight from your own traffic rather than trusting a page. |

<Callout type="warn">
  A deprecated route keeps working until its `Sunset` date and then answers
  `410 Gone`, so an integration left untouched keeps issuing invoices right up to
  a date it was told about in every response it ever received. Nothing degrades
  silently in between: the headers are there from day one.
</Callout>

## Find your route

Every route this release touched, grouped by resource. All 82 keep answering and
carry `Deprecation` and `Sunset`, and all but the four replacing `PUT`s also
carry `Link: <successor>; rel="successor-version"` — migrate before the date
`Sunset` announces. Both columns show the route in full. Where the **Now** cell
leads with a method, the method changed too, and a path-only find-and-replace
leaves you calling a verb the successor does not serve. Read the note on those
rows before you move them: `PUT` becoming `PATCH` is harmless on invoices and a
change of behaviour on customers, products and recurring invoices.

### Invoices

24 operations

The `bulk/*` routes split by intent; each successor has its own response.

| Method | Was | Now |
|---|---|---|
| `GET` `POST` | `/v1/invoices` | `/v1/companies/{company_id}/invoices` |
| `GET` `DELETE` | `/v1/invoices/{invoice_id}` | `/v1/companies/{company_id}/invoices/{invoice_id}` |
| `PUT` | `/v1/invoices/{invoice_id}` | `PATCH` `/v1/companies/{company_id}/invoices/{invoice_id}`. Only the verb and the path change. This `PUT` never replaced the invoice — it applied just the fields present in the body, exactly as the successor `PATCH` does. |
| `POST` | `/v1/invoices/{invoice_id}/issue` | `/v1/companies/{company_id}/invoices/{invoice_id}/issue` |
| `POST` | `/v1/invoices/{invoice_id}/void` | `/v1/companies/{company_id}/invoices/{invoice_id}/void` |
| `POST` | `/v1/invoices/{invoice_id}/corrective` | `/v1/companies/{company_id}/invoices/{invoice_id}/corrective` |
| `POST` | `/v1/invoices/{invoice_id}/send` | `/v1/companies/{company_id}/invoices/{invoice_id}/send` |
| `POST` | `/v1/invoices/{invoice_id}/convert-to-invoice` | `/v1/companies/{company_id}/invoices/{invoice_id}/convert-to-invoice` |
| `GET` | `/v1/invoices/{invoice_id}/pdf` | `/v1/companies/{company_id}/invoices/{invoice_id}/pdf` |
| `GET` | `/v1/invoices/{invoice_id}/pdf/preview` | `/v1/companies/{company_id}/invoices/{invoice_id}/pdf/preview` |
| `POST` | `/v1/invoices/{invoice_id}/mark-paid` | `PUT` `/v1/companies/{company_id}/invoices/{invoice_id}/status`. Target status goes in the body. |
| `POST` | `/v1/invoices/{invoice_id}/mark-sent` | `PUT` `/v1/companies/{company_id}/invoices/{invoice_id}/status`. Target status goes in the body. |
| `POST` | `/v1/invoices/{invoice_id}/revert-to-issued` | `PUT` `/v1/companies/{company_id}/invoices/{invoice_id}/status`. Target status goes in the body. |
| `POST` | `/v1/invoices/{invoice_id}/schedule` | `PUT` `/v1/companies/{company_id}/invoices/{invoice_id}/schedule` |
| `PATCH` | `/v1/invoices/{invoice_id}/reschedule` | `PUT` `/v1/companies/{company_id}/invoices/{invoice_id}/schedule`. Same route as `schedule`. |
| `POST` | `/v1/invoices/{invoice_id}/unschedule` | `DELETE` `/v1/companies/{company_id}/invoices/{invoice_id}/schedule` |
| `POST` | `/v1/invoices/{invoice_id}/duplicate` | `POST` `/v1/companies/{company_id}/invoices/derivations` |
| `POST` | `/v1/invoices/{invoice_id}/create-recurring` | `POST` `/v1/companies/{company_id}/recurring-invoices/derivations` |
| `POST` | `/v1/invoices/bulk/pdf` | `POST` `/v1/companies/{company_id}/invoices/pdf-archive` |
| `POST` | `/v1/invoices/bulk/send` | `POST` `/v1/companies/{company_id}/invoices/deliveries` |
| `POST` | `/v1/invoices/bulk/status` | `POST` `/v1/companies/{company_id}/invoices/batches` |
| `POST` | `/v1/invoices/export/excel` | `POST` `/v1/companies/{company_id}/invoices/exports` |

### Configuration

14 operations

| Method | Was | Now |
|---|---|---|
| `GET` `POST` | `/v1/configuration/series` | `/v1/companies/{company_id}/series` |
| `PATCH` `DELETE` | `/v1/configuration/series/{series_id}` | `/v1/companies/{company_id}/series/{series_id}` |
| `POST` | `/v1/configuration/series/{series_id}/default` | `PUT` `/v1/companies/{company_id}/series/{series_id}/default` |
| `POST` | `/v1/configuration/series/defaults` | `PUT` `/v1/companies/{company_id}/series/defaults` |
| `GET` | `/v1/configuration/series/defaults-status` | `GET` `/v1/companies/{company_id}/series/defaults` |
| `GET` `PUT` | `/v1/configuration/verifactu` | `/v1/companies/{company_id}/verifactu-configuration` |
| `GET` `PUT` | `/v1/configuration/taxes` | `/v1/companies/{company_id}/tax-configuration` |
| `PUT` | `/v1/configuration/language` | `PATCH` `/v1/me`. The preferred language belongs to the person, not to a NIF. |
| `GET` | `/v1/configuration/tax-types` | `GET` `/v1/tax-types`. The catalogue is the same for every NIF, so it left `/v1/configuration/`. |
| `GET` | `/v1/configuration/invoice-customization-options` | `GET` `/v1/invoice-customization-options` |

### Recurring invoices

12 operations

| Method | Was | Now |
|---|---|---|
| `GET` `POST` | `/v1/recurring-invoices` | `/v1/companies/{company_id}/recurring-invoices` |
| `GET` `PATCH` `DELETE` | `/v1/recurring-invoices/{recurring_invoice_id}` | `/v1/companies/{company_id}/recurring-invoices/{recurring_invoice_id}`. The 1:1 move: the successor `PATCH` merges the fields you send, just as this one does. |
| `PUT` | `/v1/recurring-invoices/{recurring_invoice_id}` | `PATCH` `/v1/companies/{company_id}/recurring-invoices/{recurring_invoice_id}`. **No declared successor** — the contract marks this route `x-no-successor` and the API sends it no `Link`. It replaces the schedule, lines and recipient, clearing every field you omit; the `PATCH` shown merges instead, so moving there changes what your call does. Send the template complete, with `null` in the fields you want cleared. |
| `POST` | `/v1/recurring-invoices/{recurring_invoice_id}/pause` | `PUT` `/v1/companies/{company_id}/recurring-invoices/{recurring_invoice_id}/status`. Target status goes in the body. |
| `POST` | `/v1/recurring-invoices/{recurring_invoice_id}/resume` | `PUT` `/v1/companies/{company_id}/recurring-invoices/{recurring_invoice_id}/status`. Target status goes in the body. |
| `POST` | `/v1/recurring-invoices/{recurring_invoice_id}/skip` | `/v1/companies/{company_id}/recurring-invoices/{recurring_invoice_id}/skip` |
| `POST` | `/v1/recurring-invoices/{recurring_invoice_id}/generate` | `/v1/companies/{company_id}/recurring-invoices/{recurring_invoice_id}/generate` |
| `GET` | `/v1/recurring-invoices/{recurring_invoice_id}/history` | `/v1/companies/{company_id}/recurring-invoices/{recurring_invoice_id}/history` |
| `GET` | `/v1/recurring-invoices/{recurring_invoice_id}/preview` | `GET` `/v1/companies/{company_id}/recurring-invoices/{recurring_invoice_id}/next-occurrence` |

### Customers

11 operations

| Method | Was | Now |
|---|---|---|
| `GET` `POST` | `/v1/customers` | `/v1/companies/{company_id}/customers` |
| `GET` `DELETE` | `/v1/customers/{customer_id}` | `/v1/companies/{company_id}/customers/{customer_id}` |
| `PUT` | `/v1/customers/{customer_id}` | `PATCH` `/v1/companies/{company_id}/customers/{customer_id}`. **No declared successor** — the contract marks this route `x-no-successor` and the API sends it no `Link`. It replaces the customer, clearing every field you omit; the `PATCH` shown merges instead, so moving there changes what your call does. Send the customer complete, with `null` in the fields you want cleared. |
| `PATCH` | `/v1/customers/{customer_id}` | `/v1/companies/{company_id}/customers/{customer_id}`. The 1:1 move: this `PATCH` already existed and its successor behaves identically. |
| `POST` `DELETE` | `/v1/customers/bulk` | `/v1/companies/{company_id}/customers/bulk` |
| `POST` | `/v1/customers/import-csv-preview` | `POST` `/v1/companies/{company_id}/customers/imports/preview` |
| `POST` | `/v1/customers/import-holded-contacts` | `POST` `/v1/companies/{company_id}/customers/imports` |
| `POST` | `/v1/customers/templates/csv` | `GET` `/v1/templates/customer-import`. Reading a fixed template is a `GET`, and it is the same for every NIF. |

### Products

9 operations

| Method | Was | Now |
|---|---|---|
| `GET` `POST` | `/v1/products` | `/v1/companies/{company_id}/products` |
| `GET` `DELETE` | `/v1/products/{product_id}` | `/v1/companies/{company_id}/products/{product_id}` |
| `PUT` | `/v1/products/{product_id}` | `PATCH` `/v1/companies/{company_id}/products/{product_id}`. **No declared successor** — the contract marks this route `x-no-successor` and the API sends it no `Link`. It replaces the product, resetting every field you omit to its creation default; the `PATCH` shown merges instead, so moving there changes what your call does. Send the product complete, with `null` in the fields you want cleared. |
| `PATCH` | `/v1/products/{product_id}` | `/v1/companies/{company_id}/products/{product_id}`. The 1:1 move: this `PATCH` already existed and its successor behaves identically. |
| `POST` `DELETE` | `/v1/products/bulk` | `/v1/companies/{company_id}/products/bulk` |
| `GET` | `/v1/products/search` | `GET` `/v1/companies/{company_id}/products`. **Different response shape.** The old route returned `data: [ … ]`; the successor returns `data: { products, pagination }`. |

### Webhooks

8 operations

Webhook subscriptions belong to the account, so they move under `@`.

| Method | Was | Now |
|---|---|---|
| `GET` `POST` | `/v1/webhooks` | `/v1/accounts/{account_id}/webhooks` |
| `GET` `PATCH` `DELETE` | `/v1/webhooks/{webhook_id}` | `/v1/accounts/{account_id}/webhooks/{webhook_id}` |
| `POST` | `/v1/webhooks/{webhook_id}/secret` | `/v1/accounts/{account_id}/webhooks/{webhook_id}/secret` |
| `GET` | `/v1/webhooks/{webhook_id}/deliveries` | `/v1/accounts/{account_id}/webhooks/{webhook_id}/deliveries` |
| `POST` | `/v1/webhooks/{webhook_id}/deliveries/{delivery_id}/retry` | `/v1/accounts/{account_id}/webhooks/{webhook_id}/deliveries/{delivery_id}/retry` |

### Emails

2 operations

| Method | Was | Now |
|---|---|---|
| `GET` | `/v1/emails` | `/v1/accounts/{account_id}/emails` |
| `GET` | `/v1/emails/indicators` | `/v1/accounts/{account_id}/email-indicators`. A sibling of the collection, not a member of it, so it does not reserve `indicators` as an id. |

### Request logs

2 operations

| Method | Was | Now |
|---|---|---|
| `GET` | `/v1/developers/request-logs` | `/v1/accounts/{account_id}/request-logs` |
| `GET` | `/v1/developers/request-logs/{request_id}` | `/v1/accounts/{account_id}/request-logs/{request_id}` |

## Hand it to Claude Code

This note can tell you *what* moved. Only something looking at your code can tell you *where*. If you use [Claude Code](https://claude.com/claude-code), the `beel-api` plugin has a skill that reads this note and your repository and comes back with the list.

Run `/beel-api:upgrade`. From the `beel-api` plugin. It reads `/api/changelog`, matches the route table against your own calls, and writes up what it finds. Run it with no arguments for everything, or name an area (`/beel-api:upgrade invoices`) to narrow it.

Or paste this prompt:

```text
Migrate my BeeL. API integration to the per-NIF routes.

First, read https://docs.beel.es/api/changelog and find the entry whose slug is
"resources-under-the-nif". Treat its `routeMigration` groups as the authoritative
old-to-new mapping, and its `breakingChanges` as the list of changes that a
find-and-replace will get wrong.

Then find every BeeL. API call in this codebase and report, before editing anything:

1. Each call to a DEPRECATED route — give me the successor and the Sunset date
   read from the response headers.
2. Calls that need more than a new path: a different HTTP verb (PUT becoming
   PATCH), a different request body, or a different response shape.

Propose a diff for (1). For (2), show me each call site and stop for my review —
do not rewrite them automatically.

Finally, tell me which of my calls need no change at all, so I know the scope.
```

**What to expect:**

- **It proposes; it does not apply.** You get a report and a diff to review, not a rewritten branch. Nothing changes until you say so.
- **It reads the route table rather than guessing.** The old-to-new mapping, the `Sunset` date and the traps all come from this note, served as JSON at `/api/changelog` — so it migrates to the route we actually published.
- **It tells you what is already fine.** Most of the answer is usually the list of calls that need nothing, and that is the part that lets you size the job.

**Review these by hand:**

- **`GET /v1/products/search`.** Its successor `GET /v1/companies/{company_id}/products` takes the same filter but returns `data: { products, pagination }` where the old route returned `data: [ … ]`. The path swap compiles and the mapping breaks at runtime.
- **`PUT` becoming `PATCH` — only one of these is safe.** `PUT /v1/invoices/{invoice_id}` has a declared successor and always applied just the fields you sent, so the move to `PATCH /v1/companies/{company_id}/invoices/{invoice_id}` changes the verb and the path and nothing else. The `PUT`s on customers, products and recurring invoices are the opposite: they replace the resource, they are marked `x-no-successor` in the contract, and no `Link` header points anywhere. Migrating them to the company-scoped `PATCH` turns a replace into a merge — review each call site and resend the resource complete, with `null` in the fields you want cleared.
- **Converged action verbs.** `mark-paid`, `mark-sent`, `revert-to-issued`, `pause` and `resume` collapse onto a single `status` route with the target state in the body — a change of request shape, not just of URL.

## See also

- [Multi-NIF](/multi-nif) — the model behind the axis: one account, many fiscal identities.
- [Companies](/multi-nif/companies) — creating and addressing each NIF.
- [Payment connections](/multi-nif/payment-connections) — Stripe per NIF.
- [Managed accounts](/multi-nif/managed-accounts) — issuing on behalf of another account.
- [VeriFactu auto-submit](/verifactu/auto-submit) — what changes for fiscal submission per NIF.
- [Node/TypeScript SDK](https://www.npmjs.com/package/@beel_es/sdk) — the official typed client, generated from this contract, so it targets the per-NIF routes.

---

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