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.
Creating and listing NIFs hangs off the account —
/v1/accounts/{account_id}/companies — because that is where a NIF is born; the
{account_id} is yours or one you manage. Everything you
then do to one NIF is addressed by the NIF itself: /v1/companies/{company_id}/…. See
Where each identifier goes.
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": "B12345674",
"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 must match the NIF: an individual has a DNI or NIE, an entity a NIF starting with the letter of its legal form; a mismatch answers422ENTITY_TYPE_INCONSISTENT_WITH_NIF. 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 with422NUMBERING_REQUIRES_ACTIVATION— there are no series yet to number.default_irpf_rateis a declaration, not a default. See the warning below. It must be a rate the company can bear by its NIF: a company (B…) declaring 15 % answers422IRPF_RATE_NOT_FOR_CORPORATE_ISSUER(see IRPF).
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 — in the sandbox too. A syntactically valid NIF
that is not registered is 422 NIF_VALIDATION_INVALID; one registered
as revoked is 422 NIF_REVOCADO. Use your client's real NIF, even to
rehearse.
The aeat_environment field decides the flow
| Case | Result |
|---|---|
aeat_environment: "TEST" | Created immediately, free. Sandbox NIF; invoices reach VeriFactu test. 201 + the company, with every field GET /v1/companies/{company_id} returns, plus its series. |
aeat_environment: "PROD", account entitled to Live — a card on file, or an active paid plan | 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 CHECKOUT_REQUIRED, with no checkout URL. |
aeat_environment: "PROD", account past due | No company is created. 402 PAYMENT_REQUIRED. Settle the outstanding invoice first. |
aeat_environment: "PROD", account on a fixed-quota plan still on trial | No company is created. 402 PLAN_ACTIVATION_REQUIRED: activate the plan you already chose; no card checkout applies, because the quota already includes this NIF. |
This endpoint never starts a charge. A 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 — on purpose: a live key can only be created once the account has Live, so the
first activation in Live has to come from a test key (or from the dashboard).
Switching a NIF on in Live is billable, whatever key you use. A test key cannot create Live invoices, but it can create a Live NIF, and that NIF counts towards your subscription from that moment.
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"
}'It returns 201. TEST is immediate and free. PROD is immediate when the account is
entitled to Live, and the NIF is added to the existing subscription. Otherwise nothing is
switched on and the call answers 402, with the same three codes as creation:
CHECKOUT_REQUIRED— no card on file. When you passedsuccess_urlandcancel_url,error.details.checkout_urlis the checkout to hand to the account holder.success_urlmay embed Stripe's{CHECKOUT_SESSION_ID}placeholder.PAYMENT_REQUIRED— past due. Settle the outstanding invoice first.PLAN_ACTIVATION_REQUIRED— a fixed-quota plan still on trial. Activate the plan.
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=TEST or …=PROD;
environment is required (400 MISSING_PARAMETER without it). In
Test it is immediate. In Live it is scheduled: 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:
409NIF_ALREADY_REGISTEREDis not a dead end. The existing id travels inerror.details.company_id— switch that NIF on instead of creating it again.409NIF_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.NIF_VALIDATION_INVALIDandNIF_REVOCADOare about the NIF itself, not your request: no retry fixes them.502EXTERNAL_SERVICE_ERRORmeans the AEAT census could not be reached to validate the NIF. Nothing was created; repeat the same request later, with the sameIdempotency-Key.422POSTAL_CODE_INVALID_ES: a Spanish postal code, inaddressor inlegal_representative.address, does not have 5 digits. Nothing was created. Other countries' postal codes are free-form.- A
403never distinguishes "you cannot reach it" from "it does not exist". Do not treat it as a signal that the NIF is free.
The company is created with everything you send: trade_name, the whole address (floor,
door and country included) and the legal representative's.
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, for
the environment of your key, and a separate verifactu block. How to read it, and each
blocker's meaning and fix, is in Is a NIF ready to invoice?;
the three VeriFactu ones in detail in
Enabling VeriFactu. 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. Most fields (trade name, address, legal representative, contact
details, IBAN, PDF template…) are editable — see
the reference for the full editable set. The exceptions:
nif,entity_typeandlegal_formare immutable. Sending a different value answers422IMMUTABLE_NIF,IMMUTABLE_ENTITY_TYPEorIMMUTABLE_LEGAL_FORM, and nothing is written; sending the value it already has is not a change.legal_nameonly changes together with an AEAT census re-validation. For a legal entity the census identifies the company by its NIF alone, so the name you send cannot make it fail; for anINDIVIDUALthe name must match the census. If the census cannot be reached, the change is not rejected: the response is200with the new name stored, and the check is repeated in the background.- A Spanish postal code must have 5 digits, in
addressand inlegal_representative.address— otherwise422POSTAL_CODE_INVALID_ES. - Access
VIEWover the NIF reads it but cannot change it: a write answers403COMPANY_READ_ONLY.
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
(400 CANNOT_DELETE_PRIMARY), nor a company that holds invoices in Live.
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 by the NIF's holder and submitted. That flow lives under
/v1/companies/{company_id}/representation:
| Step | Call | Notes |
|---|---|---|
| Generate | POST …/representation | Either key. Needs a complete fiscal identity — for a legal entity, the legal representative's full address too; otherwise PROFILE_INCOMPLETE, with the missing fields in error.details. |
| Download | GET …/representation/document | Either key. A short-lived download_url to the PDF the holder signs. |
| Submit the signed copy | POST …/representation/submit | Live key only — the representation has no test mode (400 SIGNING_ONLY_IN_LIVE with a test key). multipart/form-data with the PDF in file. A PDF without a valid signature is 400 PDF_SIGNATURE_INVALID; a file that is not a PDF, 400 PDF_INVALID_EXTENSION. |
| Status | GET …/representation | Never fails: NOT_STARTED, PDF_GENERATED, ACTIVE or CANCELLED. SUBMITTED is only the acknowledgement of the submit call, and ERROR is reserved. |
| Cancel | DELETE …/representation | Cancels an active (signed) representation. Before that there is nothing to cancel, and it answers 400. |
The document authorises BeeL. to submit the NIF's billing records to AEAT on the taxpayer's behalf — this is how BeeL. submits without the taxpayer handing over a digital certificate. The holder (the self-employed person, or the legal representative of a company) signs the PDF electronically with their own certificate, once. Everything else — generating, downloading, uploading and checking the status — is an API call, so a new issuer can be onboarded entirely from your product except for that signature.
The holder signs; whoever holds OPERATE over the NIF — the holder, or you as its
provisioner — uploads the signed copy. See
Generate the representation document and Submit the signed representation document.
Related
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.
Connect Stripe
Connect Stripe per company (NIF) so its payments auto-generate invoices under the right NIF — for the companies you own and, white-label, for the managed accounts you provision.