NewTell a voided invoice from a totally rectified one, without a second call
BeeL
Get StartedMulti-NIFVeriFactuStripeAPI ReferenceChangelog

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 (INDIVIDUAL or LEGAL_ENTITY) is immutable after creation. It is not a label: it decides whether legal_form and legal_representative are accepted at all. Getting it wrong means deleting the NIF and starting again.
  • aeat_environment picks which AEAT environment the NIF is registered against, and with it the whole billing branch — see The aeat_environment field decides the flow.
  • activate is 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; false creates the profile only, and you switch it on later with the activations endpoint. A numbering block without an activation is rejected with 422 NUMBERING_REQUIRES_ACTIVATION — there are no series yet to number.
  • default_irpf_rate is 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

CaseResult
aeat_environment: "TEST"Created immediately, free. Sandbox NIF; invoices reach VeriFactu test. 201 + the company.
aeat_environment: "PROD", account already production-activeCreated 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 fileNo company is created. 402 with error.code=CHECKOUT_REQUIRED and no checkout URL.
aeat_environment: "PROD", account past dueNo 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}/activationscompanies: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_REGISTERED is not a dead end. The existing id travels in error.details.company_id — switch that NIF on instead of creating it again.
  • NIF_PROD_ALREADY_ACTIVE_IN_ANOTHER_ACCOUNT is structural: a NIF can only issue in production from one account. No retry fixes it; the other account has to release it first.
  • A 403 never 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:

  • id is 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 its nif string.
  • environment is deprecated and cannot be trusted. A single scalar cannot express a NIF that is switched on in both modes. Read the in_test / in_prod pair instead.
  • default_irpf_rate absent is not default_irpf_rate: 0. Absent means nobody ever declared a rate; 0 means someone declared exemption. Do not coalesce one into the other when you render or re-send it. It is read-only here — change it with PUT /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/statscompanies: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-readinesscompanies: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