Overview
The multi-NIF model in one place — accounts, companies (NIFs), members with grants and managed accounts; which credential acts on what, what needs a live key, and what the errors mean.
Your client is a freelancer who also runs an S.L., so they invoice under two tax IDs. An accounting firm (gestoría) runs the invoicing of forty clients. Both are the same model, and this page explains it once; every other page in this section links back here.
The model
graph LR A["Your account<br/>account_id"] -->|holds 1..N| C["Company = one NIF<br/>company_id"] A -->|has| M["Members<br/>OWNER · ADMIN · MEMBER"] M -.->|"grant VIEW / OPERATE<br/>(MEMBER only)"| C A -->|"manages, at an access_level"| B["Managed account<br/>provisioned by you"] B -->|holds 1..N| D["Company = one NIF"]
Four pieces, in this order:
- Account — who pays and authenticates. It owns the subscription, the API keys, the
webhooks and the people. Identified by
account_id. - Companies — each NIF/CIF the account invoices under, with its own invoices,
customers, products and series, kept apart in Test and Live. Identified by
company_id. Throughout the docs and the API reference, Company always means one NIF. → Companies - Members and grants — the people with access to the account. An
OWNERorADMINreaches every NIF; aMEMBERreaches only the NIFs they hold a grant on, atVIEWorOPERATE. → Members & grants, Invitations - Managed accounts — separate accounts that you provisioned for someone else. You
pay for them and hold one
access_levelover each (NONE,VIEWorOPERATE); the holder can claim theirs later. → Managed accounts
An account is not a NIF: the account is who pays and authenticates, the NIF is who invoices. A freelancer with one NIF has one account and one company — the distinction only shows up at two.
Member or managed account?
Both give you access to somebody else's NIFs, and they are not interchangeable:
- The taxpayer signs up and pays for themselves, and wants your help → they invite you as a member of their account. Their account, their bill; you hold a role in it.
- You onboard them by API and you pay → you provision a managed account. Your bill; they can claim the account later without you losing your access.
Which credential acts on what
- An API key belongs to one account and is created by an
OWNERorADMINof it (aMEMBERcannot create keys:403ACCOUNT_MANAGEMENT_FORBIDDEN). It reaches every NIF of that account, plus the NIFs of the accounts it manages, at theaccess_levelheld over each. - Grants govern people in the dashboard. They decide what a
MEMBERsees when signed in; they never apply to an API key. - Scopes decide which operations a key may call; access decides where it may write. A
key with
invoices:writestill cannot issue for a NIF you only hold atVIEW. The full scope catalog, including the two provisioning scopes, is in Scopes. - The target is always the id in the path, never the credential. An API key is stateless and carries no notion of a "current" NIF.
Test and live keys
The base URL is the same; the key prefix picks the environment (Authentication).
For everything that belongs to a NIF — invoices, customers, series, readiness, payment
connections — a beel_sk_test_ key works on Test data and a beel_sk_live_ key on Live
data.
The account's control plane is different: members, grants, invitations and your management of an account are not per environment. A collaborator is the same person with the same grants in Test and Live, so a change there is real whichever key makes it. That is why these writes refuse a test key:
| Operation | Test key | Live key |
|---|---|---|
| Read members, grants and invitations | Yes | Yes |
| Invite, revoke an invitation, change a role, remove a member, set or remove a grant | 403 LIVE_CREDENTIAL_REQUIRED | Yes |
| Provision accounts, issue claim tokens, import accounts, list them, read usage | Yes | Yes |
Change your access_level over a managed account, end its management | 403 LIVE_CREDENTIAL_REQUIRED | Yes |
| Switch a NIF on or off in Test or Live | Yes — the mode travels in the body | Yes |
| Submit a signed VeriFactu representation | 400 SIGNING_ONLY_IN_LIVE | Yes |
| Hand ownership of the account over | 403 OPERATION_REQUIRES_SESSION | 403 OPERATION_REQUIRES_SESSION |
Handing ownership over is dashboard-only. No API key can do it, a live one included, so a leaked key can never take the account. See Hand over ownership.
Two identifiers
| What it is | The call that returns it | |
|---|---|---|
account_id | The account: subscription, API keys, members, webhooks. | GET /v1/me/identity → data.account_id |
company_id | One NIF. | GET /v1/accounts/{account_id}/companies → data.companies[].id |
company_id is a UUID, never the tax number itself. It is the id the companies
endpoint returns — 550e8400-e29b-41d4-a716-446655440000, not B12345674. Sending a
NIF where a company_id is expected returns 400.
# 1. Which account is this key?
curl https://app.beel.es/api/v1/me/identity \
-H "Authorization: Bearer beel_sk_live_xxx"
# 2. Which NIFs does it hold?
curl https://app.beel.es/api/v1/accounts/3fa85f64-5717-4562-b3fc-2c963f66afa6/companies \
-H "Authorization: Bearer beel_sk_live_xxx"Where each identifier goes
Both identifiers travel in the path. Which one you use depends on whether the thing you are touching belongs to a NIF or to the account.
{company_id} — everything that belongs to one NIF, both configuring it and operating
on it:
/v1/companies/{company_id}/invoices
/v1/companies/{company_id}/customers
/v1/companies/{company_id}/products
/v1/companies/{company_id}/series
/v1/companies/{company_id}/tax-configurationThe account that owns the NIF is derived from it, so there is no pair to keep coherent.
{account_id} — everything that belongs to the account: its list of NIFs, members,
invitations, the accounts it manages, webhooks and request logs.
/v1/accounts/{account_id}/companies
/v1/accounts/{account_id}/members
/v1/accounts/{account_id}/webhooksMigrating from the flat routes? If you integrated against /v1/invoices,
/v1/customers, /v1/products or /v1/configuration/series — routes with no
company_id — those pick the NIF from a BeeL-Active-Company request header. They are
deprecated: they answer with Deprecation and Sunset headers and stop working on
the date the Sunset header announces. Move the NIF out of the header and into the path;
the behaviour is otherwise identical.
When it does not add up
| When | Error | Status |
|---|---|---|
The key lacks a scope. error.details.missing_scopes names them, comma-separated. | INSUFFICIENT_SCOPE | 403 |
The {account_id} is not yours to reach — the same answer as does not exist, so existence is never disclosed. | ACCOUNT_NOT_ACCESSIBLE | 403 |
The same, for the {company_id}. Today it is also the answer to a write under /v1/companies/{company_id}/… when you only hold VIEW over that NIF; reads still work. | ACTIVE_COMPANY_NOT_ACCESSIBLE | 403 |
A write on an account-scoped route while you hold VIEW — for example adding a NIF to an account you manage at VIEW. | COMPANY_READ_ONLY | 403 |
Members and invitations need OWNER or ADMIN of that account. A provisioner is neither on the accounts it manages. | MEMBER_MANAGEMENT_FORBIDDEN | 403 |
Account settings (API keys, webhooks) need OWNER or ADMIN of that account. | ACCOUNT_MANAGEMENT_FORBIDDEN | 403 |
A control-plane write made with a beel_sk_test_ key. See Test and live keys. | LIVE_CREDENTIAL_REQUIRED | 403 |
| Handing ownership over. Dashboard only. | OPERATION_REQUIRES_SESSION | 403 |
| Provisioning without the managed-accounts capability, which BeeL. enables on request. | FEATURE_NOT_AVAILABLE | 403 |
| A member grant names a NIF of a different account — including one of the accounts you manage. | GRANT_COMPANY_NOT_IN_ACCOUNT | 422 |
Only on the deprecated flat routes: several NIFs and no BeeL-Active-Company header. It cannot happen once the NIF is in the path. | ACTIVE_COMPANY_REQUIRED | 403 |
Treat both read-only answers the same way. A write you are not allowed to make with
VIEW access comes back as COMPANY_READ_ONLY or, on NIF routes,
ACTIVE_COMPANY_NOT_ACCESSIBLE. Neither is fixed by retrying: raise the
access, or read instead.
Is a NIF ready to invoice?
curl https://app.beel.es/api/v1/companies/550e8400-e29b-41d4-a716-446655440000/issuing-readiness \
-H "Authorization: Bearer beel_sk_live_xxx"{
"success": true,
"data": {
"ready": true,
"blockers": [],
"verifactu": { "ready": false, "blockers": ["NIF_NOT_REGISTERED"] }
}
}ready is true only when blockers is empty. Two things the answer depends on:
- The environment of your key. A
beel_sk_test_key asks about Test, abeel_sk_live_key about Live, so the same NIF can be ready in one and not in the other — a NIF provisioned in Test isreadywith a test key and reportsCOMPANY_NOT_ACTIVATEDwith a live one until you switch it on there. - Which question you ask.
ready/blockerssay whether the NIF can issue.verifactu.ready/verifactu.blockerssay whether its invoices would pass VeriFactu registration — a separate question. A Live NIF can beready: truewhileverifactu.blockersstill lists what AEAT registration needs.
| Blocker | What it means | How to clear it |
|---|---|---|
COMPANY_HAS_NO_NIF | The company has no tax identification number on file. | Set the NIF on the company. |
SERIES_DEFAULT_NOT_FOUND | No default invoice series exists for the environment. | Switch the company on in that environment (it creates the default series), or mark an existing series as default. |
PROFILE_INCOMPLETE | The company's fiscal identity is not complete enough to write an invoice header (entity type, legal name or fiscal address). | Complete the entity type, legal name and fiscal address on the company. |
COMPANY_NOT_ACTIVATED | The company is not activated in the environment you are calling, and it issues without Veri*Factu. | Activate the company in that environment. |
ENV_MISMATCH | The NIF is not switched on in the environment you are calling, and its invoices go to VeriFactu there. | Activate the company in that environment. |
NIF_NOT_REGISTERED | The NIF is not registered with AEAT for Veri*Factu. | Complete the Veri*Factu configuration for the NIF. |
NIF_REPRESENTATION_REQUIRED | A signed fiscal representation from the account holder is missing. | Generate the representation, have the holder sign it, and submit it. |
Add ?include=readiness to GET /v1/accounts/{account_id}/companies to get the same answer
for every NIF in one call. See Get the issuing readiness of a company.