Companies & fiscal profile
Create, list, read, update and delete the companies (NIFs) under your account, including the TEST vs PROD creation flow.
A company is one NIF/CIF under your account, with its own fiscal profile, invoices and series. This page covers the full lifecycle — the order of the calls, what each one commits you to, and where it can fail. The exact fields, types and schemas of every operation live in the Companies API Reference.
Two shapes, one rule: the identifier in the path decides. Creating and listing
NIFs hangs off the account — /v1/accounts/{account_id}/companies — because that is
where a NIF is born. Everything you then do to one NIF is addressed by the NIF
itself: /v1/companies/{company_id}/…. Your own {account_id} comes from
GET /v1/me/identity (data.account_id), or is the id of an account you
provisioned. Neither identifier is taken from the
credential; a target you do not reach returns 403, the same answer a non-existent
one gets.
Create a company
POST /v1/accounts/{account_id}/companies — requires the companies:write
scope. By default the NIF is also switched on in the requested mode, which is
what seeds its invoice series (ordinary F, simplified S, corrective R) and its
tax defaults, so the company can issue right away — rename or edit them later in the
app. Send activate: false to create only the NIF profile.
curl -X POST https://app.beel.es/api/v1/accounts/3fa85f64-5717-4562-b3fc-2c963f66afa6/companies \
-H "Authorization: Bearer beel_sk_live_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f8c1e10-3b2a-4c9d-8e7f-1a2b3c4d5e6f" \
-d '{
"nif": "B12345678",
"legal_name": "Mi Empresa SL",
"entity_type": "LEGAL_ENTITY",
"address": {
"street": "Calle Mayor",
"number": "10",
"postal_code": "28001",
"city": "Madrid",
"province": "Madrid"
},
"legal_form": "SL",
"legal_representative": {
"full_name": "Ada Lovelace",
"nif": "12345678Z",
"address": {
"street": "Calle Mayor", "number": "10",
"postal_code": "28001", "city": "Madrid", "province": "Madrid"
}
},
"trade_name": "Mi Empresa",
"default_main_tax": { "type": "IVA", "percentage": 21, "regime_key": "01" },
"aeat_environment": "TEST"
}'The fields that decide the flow
The complete request body — every field, its type, whether it is required and its default — is in Create a company. Four of them change what the call does, and are worth understanding before you send it:
entity_type(INDIVIDUALorLEGAL_ENTITY) is immutable after creation. It is not a label: it decides whetherlegal_formandlegal_representativeare accepted at all. Getting it wrong means deleting the NIF and starting again.aeat_environmentpicks which AEAT environment the NIF is registered against, and with it the whole billing branch — see Theaeat_environmentfield decides the flow.activateis what separates "a NIF that can invoice" from "a NIF on file". Left at its default the call also seeds the series and tax defaults;falsecreates the profile only, and you switch it on later with the activations endpoint. Anumberingblock without an activation is rejected with422 NUMBERING_REQUIRES_ACTIVATION— there are no series yet to number.default_irpf_rateis a declaration, not a default. See the warning below.
Never preselect an IRPF rate. A company created without default_irpf_rate
withholds nothing because nobody declared a rate; a company with 0 withholds
nothing because its owner declared so. They invoice the same, but only one is a
declaration — do not fill the absent case in with a number of your own.
The NIF must exist in the AEAT census. A syntactically valid NIF that is not
registered is rejected with 422 — even in the sandbox.
The aeat_environment field decides the flow
| Case | Result |
|---|---|
aeat_environment: "TEST" | Created immediately, free. Sandbox NIF; invoices reach VeriFactu test. 201 + the company. |
aeat_environment: "PROD", account already production-active | Created immediately as a production NIF. Real AEAT emission stays blocked until the VeriFactu representation is signed for this NIF. 201 + the company. |
aeat_environment: "PROD", account with no card on file | No company is created. 402 with error.code=CHECKOUT_REQUIRED and no checkout URL. |
aeat_environment: "PROD", account past due | No company is created. 402 with error.code=PAYMENT_REQUIRED. Settle the outstanding invoice first. |
This endpoint never starts a charge. A 402 CHECKOUT_REQUIRED here carries no
checkout_url: capturing a card is a browser flow, and an API key has no browser to
return from. Create the company in TEST (or with activate: false) and switch it
on in Live with the activations endpoint,
which is the only door that opens a checkout.
Switching a NIF on in Test or Live
POST /v1/companies/{company_id}/activations —
companies:write. This is how a NIF that already exists reaches Live: creation
only ever switches a NIF on in one mode, so a NIF created in Test — how every
provisioned account starts — has no other route. The mode travels in the body and is
never taken from the key's environment, so a beel_sk_test_… key can switch a NIF on
in Live.
curl -X POST https://app.beel.es/api/v1/companies/550e8400-e29b-41d4-a716-446655440000/activations \
-H "Authorization: Bearer beel_sk_live_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f8c1e10-3b2a-4c9d-8e7f-1a2b3c4d5e6f" \
-d '{
"environment": "PROD",
"success_url": "https://your-app.example/billing/return?session={CHECKOUT_SESSION_ID}",
"cancel_url": "https://your-app.example/billing"
}'TEST is immediate and free. PROD is immediate when the account already has a card
on file, and the NIF is added to the existing subscription. With no card on file it
returns 402 CHECKOUT_REQUIRED and — when you passed success_url and cancel_url —
an error.details.checkout_url to hand to the account holder. success_url may embed
Stripe's {CHECKOUT_SESSION_ID} placeholder.
The call is idempotent: repeating it neither opens a second checkout nor adds a second
subscription item, and answers already_active: true. It also cancels a pending
switch-off (scheduled_deactivation_cancelled: true), with nothing charged or
credited.
Switching a NIF off is DELETE …/activations?environment=PROD. In Live it is
scheduled, not immediate: the response carries an effective_at and the NIF keeps
invoicing until then. Nothing is refunded.
When creation fails, read the code before retrying
The status codes and their bodies are on the operation page; what they mean for your next call is not:
NIF_ALREADY_REGISTEREDis not a dead end. The existing id travels inerror.details.company_id— switch that NIF on instead of creating it again.NIF_PROD_ALREADY_ACTIVE_IN_ANOTHER_ACCOUNTis structural: a NIF can only issue in production from one account. No retry fixes it; the other account has to release it first.- A
403never distinguishes "you cannot reach it" from "it does not exist". Do not treat it as a signal that the NIF is free.
List companies
GET /v1/accounts/{account_id}/companies — requires the companies:list scope.
Returns a paginated list of the companies (NIFs) of that account (query params
page, limit, search, include; primary company first). Always paginated — it
scales to thousands of NIFs. An account with no NIF yet returns an empty page, not an
error. See the reference for the response schema.
curl https://app.beel.es/api/v1/accounts/3fa85f64-5717-4562-b3fc-2c963f66afa6/companies \
-H "Authorization: Bearer beel_sk_live_xxx"Add ?include=readiness to get each company's
issuing-readiness block in the same call.
Each item is a company object — its full shape is in List companies. Three of its fields are read wrong often enough to be worth calling out:
idis the{company_id}you put in the path of every later call that touches this NIF. It is the only handle; nothing addresses a company by itsnifstring.environmentis deprecated and cannot be trusted. A single scalar cannot express a NIF that is switched on in both modes. Read thein_test/in_prodpair instead.default_irpf_rateabsent is notdefault_irpf_rate: 0. Absent means nobody ever declared a rate;0means someone declared exemption. Do not coalesce one into the other when you render or re-send it. It is read-only here — change it withPUT /v1/companies/{company_id}/tax-configuration.
Two independent axes — don't conflate them. The activation pair
(in_test / in_prod) is the data/billing mode, matching the dashboard's Test/Live
switch. AEAT emission capability is a separate axis (account_state,
verifactu_status, and the readiness block). A company can bill in Live and still
have AEAT emission disabled until its VeriFactu representation is signed.
Per-company stats
GET /v1/accounts/{account_id}/companies/stats — companies:list. Returns
invoice aggregates per company (company_id, invoice_count, last_invoice_at),
kept separate from the list so the company switcher stays cheap.
Read a single company
GET /v1/companies/{company_id} — requires companies:read. Returns the same
company object as above, including VeriFactu status. The NIF in the path is the only
source of context: the account that owns it is derived from it, so there is no pair to
keep coherent.
Is this NIF ready to issue?
GET /v1/companies/{company_id}/issuing-readiness —
companies:read. Returns ready plus, when it is false, the exact blockers:
COMPANY_HAS_NO_NIF, SERIES_DEFAULT_NOT_FOUND, ENV_MISMATCH,
NIF_NOT_REGISTERED or NIF_REPRESENTATION_REQUIRED. Readiness is per NIF; it does
not cover the payer account's quota or a specific invoice's payload.
Update a company
PATCH /v1/companies/{company_id} — requires
companies:write. nif, entity_type and legal_form are immutable;
legal_name only changes together with a successful AEAT census re-validation (for a
legal entity that re-validation checks the CIF only — the name is not verified, so it
cannot fail because of the name you send). Most other fields (trade name, address,
legal representative, contact details, IBAN, PDF template…) are editable — see
the reference for the full editable set.
curl -X PATCH https://app.beel.es/api/v1/companies/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer beel_sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"trade_name": "Mi Empresa (Madrid)",
"address": {
"street": "Gran Vía",
"number": "1",
"postal_code": "28013",
"city": "Madrid",
"province": "Madrid",
"country_code": "ES"
}
}'Delete a company
DELETE /v1/companies/{company_id} — requires
companies:write. Returns 204. You cannot delete your primary company.
A company that is switched on in Live cannot be deleted either: it returns 409
COMPANY_ACTIVE_IN_PRODUCTION. Leaving Live is a scheduled deactivation — the
current cycle is charged and served in full — so switch it off first and delete it
once the deactivation takes effect. Companies never activated, or active only in
Test, are deleted right away.
Signing the VeriFactu representation
A production NIF only reaches the real AEAT once its VeriFactu representation is
generated, signed and submitted. That flow lives under
/v1/companies/{company_id}/representation: POST generates the document, GET …/representation/document downloads it, POST …/representation/submit uploads the
signed copy, GET …/representation reports the status and DELETE …/representation
cancels it. See the Companies API Reference.
Next steps
Overview
Two identifiers — account_id and company_id — how to get each one, how they build a URL, and what the errors mean when the pair does not add up.
Connect Stripe
Connect Stripe per company (NIF) so its payments auto-generate VeriFactu-compliant invoices under the right NIF — for the companies you own and, white-label, for the managed accounts you provision.