---
title: "Import managed accounts from a file"
method: POST
path: "/v1/accounts/imports"
tags: ["AccountImports"]
---

# Import managed accounts from a file

`POST /v1/accounts/imports`

## Request

- Base URL: `https://app.beel.es/api`
- URL: `https://app.beel.es/api/v1/accounts/imports`
- Auth: HTTP bearer

Provisions the managed accounts described in an uploaded file, switches each one on in
Live, and leaves them ready to invoice. It is the same act as calling `POST /v1/accounts`
once per row and then `POST /v1/companies/{company_id}/activations`, with the bookkeeping
done for you.

## Idempotency and re-runs

- **Not atomic:** each row is processed and reported independently, and a row that fails
  leaves the rows already provisioned in place. `statistics.accounts_created` is how many
  accounts this call actually created.
- **Declarative and re-runnable:** each pass applies only what is missing — an
  `external_ref` you already provisioned is reconciled, not duplicated, and so are its
  series and its customers. That is the recovery path for anything that went wrong: fix the
  cause and upload the same file again; there is no resume and no partial state to clean
  up.
- **`Idempotency-Key`:** required, but the real guarantee is in the data. Rows are
  idempotent by `external_ref`, so the same file uploaded twice creates nothing twice even
  under a different key.
- **Dry run:** to see what this would do without writing anything, use
  `POST /v1/accounts/imports/preview`, a separate operation with no effects at all — the
  import is never governed by a boolean flag.

## Files and limits

- **`accounts_file`:** describes the accounts, one per row.
- **`customers_file`:** optional, and holds a list of customers applied to **every** account
  of the import, new and pre-existing alike, so a new managed account is born knowing all
  the customers and a new customer reaches all the accounts on the next pass. It is the
  same CSV that `GET /v1/templates/customer-import` describes, and it is idempotent by tax
  id.
- **`options.apply_customers_to_own_company`:** lands those customers on your own company
  as well — the one in focus, never one chosen for you. That outcome comes back apart, in
  `own_company_customers`, and stays out of `statistics.customers_created`.
- **Limits:** 5 MB per file, 100 rows in the accounts file and 1,000 in the customers file.
  A larger population is imported in passes, which costs nothing because the file is
  declarative.

## Live activation

Live activation is part of the act: every row is weighed against the same verdict the
account state publishes, and only rows entitled to Live are executed; the rest come back
`BLOCKED` with the reason. The import never opens a checkout, so it never charges you by
surprise: settle your billing once and re-upload.

If your agreement caps the NIFs you can have in Live, each row switched on takes a place, in
file order; the rows that no longer fit come back `BLOCKED` with `NIF_LIMIT_REACHED`, the
same code `POST /v1/companies/{company_id}/activations` answers with.

## Claim tokens

`account.claim_token` and `account.claim_url`: each newly provisioned row carries them
in this response and nowhere else, so persist them before discarding it. A lost token is
re-issued with `POST /v1/accounts/{account_id}/claim-tokens`.

Re-uploading the file never breaks the links you already handed out: a row whose account is
still unclaimed and holds a valid claim link comes back with `claim_token` and `claim_url`
`null` and `account.claim_link_already_issued: true`. Only an account with no valid link left
(expired, or never issued) gets a fresh one.

## Headers

- `Idempotency-Key` string, required

## Response `201`

The import ran, in the same format as the preview. The status is fixed: it reports that the import ran, not that any particular row succeeded. `metadata.is_dry_run` is `false`, and the write counters in `statistics` are what actually reached the database — they can all be `0` when every row was already there or blocked.

- object: The envelope every successful JSON response of the BeeL. API is wrapped in. There are no bare resources in v1 and none are planned: the payload always hangs off `data`. - A **single resource** is an object in `data`. - A **collection** hangs off a named key inside `data`, together with its `pagination`, also inside `data` — `data: {invoices: [...], pagination: {...}}`. - A collection carries `pagination` unless its operation declares itself a **closed catalogue**: a fixed, bounded list with nothing to page through. The declaration is explicit in the operation; a missing `pagination` is never something to infer. - `GET /v1/accounts` pages by cursor (`data: {accounts: [...], next_cursor}`). It is a documented variant of pagination, not another envelope. Putting the array straight into `data` with `pagination` as its sibling is the shape a v2 would adopt; v1 is not being flipped to it.
  - `success` boolean, required
  - `data` object, required: The outcome of the file, row by row, in one shape for the preview and for the import. Which one produced it is `metadata.is_dry_run`; what it actually wrote is the write counters in `statistics`.
    - `metadata` AccountImportMetadata, required
      - `is_dry_run` boolean, required: `true` for the preview (nothing was written), `false` for the import. It is what tells the two answers apart when they are stored side by side.
      - `total_rows` integer, required: Data rows found in the accounts file, header excluded.
      - `processing_time_ms` integer, required
      - `accounts_filename` string, nullable: Name of the uploaded accounts file, as your client sent it.
      - `customers_filename` string, nullable: Name of the uploaded customers file; `null` when none was sent.
      - `environment` 'TEST' | 'PROD', required: Mode a record lives in — its Test/Live twin. It decides where invoices, customers and quota are accounted. For a company it also decides which AEAT its NIF is registered against: switching a company on in `PROD` is what registers it with the real AEAT, so `aeat_environment` is that same mode and uses this same enum.
    - `accounts_validation` AccountImportItem[], required: One entry per data row of the accounts file, in file order and **never filtered**: a row that failed keeps its place and its `row_number`, so the answer lines up with the spreadsheet.
      - `row_number` integer, required: Row number **as the spreadsheet counts it**: the header is row 1, so the first account is row 2. Not an index — the point is that you can open the file and go to that line.
      - `external_ref` string, nullable: Your own id for the account; `null` when the row was too malformed to read it.
      - `nif` string, nullable: The row's tax id, normalised; `null` when it could not be read.
      - `status` 'VALID' | 'WARNING' | 'ALREADY_EXISTS' | 'BLOCKED' | 'ERROR', required: Outcome of one row. The same five values are used by the preview and by the import — the preview reports what the import would reach — so no value tells you which operation produced the payload; `metadata.is_dry_run` does. * `VALID` — complete and reachable. Provisioned by the import; would be by a preview. * `WARNING` — the same, with non-blocking warnings. * `ALREADY_EXISTS` — an account with this `external_ref` is already yours. A documented success, not a collision: provisioning is idempotent by `external_ref`, so nothing is created and nothing is billed again. On an import the row is still reconciled (Live, series, customers), which is what makes re-uploading the same file useful. * `BLOCKED` — well formed but not executable. `errors` says why, and it is always something to resolve outside the file. * `ERROR` — the row's own data is wrong. `errors` says which column.
      - `live_activation_verdict` 'ENTITLED' | 'CHECKOUT_REQUIRED' | 'PLAN_ACTIVATION_REQUIRED' | 'PAYMENT_REQUIRED' | 'NOT_BILLING_SUBJECT' | 'BILLING_MANAGED_EXTERNALLY' | 'CONTACT_SALES': The server's complete answer to "may THIS actor activate a NIF in Live on this account, and if not, why?". Published so the UI never has to re-derive what the server will decide when the button is pressed — a screen that computes it on its own ends up disagreeing, and the disagreement shows up as a green button that answers 403. The first four values are predicated on the PAYING account (a managed account inherits its manager's entitlement); the last two on the ACTOR and the account in focus. * `ENTITLED` — live plan/contract or card on file. Activation is immediate. * `CHECKOUT_REQUIRED` — no card yet: go through the checkout. * `PLAN_ACTIVATION_REQUIRED` — fixed-quota plan on trial: the act is ACTIVATING THE PLAN, not the per-NIF checkout (that one charges for a NIF the quota already includes). * `PAYMENT_REQUIRED` — unpaid (payment retries exhausted): no new checkout, regularise the existing one. * `NOT_BILLING_SUBJECT` — the actor is not the billing subject. Informative, no action. * `BILLING_MANAGED_EXTERNALLY` — an agency handles the billing. Informative, no action. * `CONTACT_SALES` — API plan account with two or more active NIFs and no agreed price: going Live (checkout or activation) needs a price agreed with sales first. The activation answers `403 CONTACT_SALES_REQUIRED` with `details.reason = LIVE_WITH_MULTIPLE_NIFS` and `details.salesUrl`.
      - `account` ProvisionAccountResult: Result of a successful account provision. `claim_token` is a single-use secret to deliver to the account holder so they can set their password and take ownership. It is returned only once; `null` if the account was already claimed, `null` when the account was provisioned without `email` (there is no holder to hand it to yet), and `null` when a repeated provision found a claim link still valid (`claim_link_already_issued: true`). `claim_url` is the ready-to-use claim link BeeL builds for the current environment; `status` disambiguates a `null` `claim_token` (already claimed) from an error.
        - `person_id` string, uuid, nullable: The account holder. `null` when the account was provisioned **without `email`**: it has no person yet, and gains one when a claim token is issued.
        - `account_id` string, uuid, required
        - `status` 'PROVISIONED' | 'CLAIMED' | 'ACTIVE', required: Lifecycle stage of a provisioned account. The claim gates the stage: `PROVISIONED` (created, not yet claimed — the holder has not set a password / taken ownership, even if a NIF was seeded at provisioning or you invoice on their behalf with `OPERATE`); `CLAIMED` (the holder set their password and took ownership, no NIF yet); `ACTIVE` (claimed and has at least one NIF — can operate under their own ownership).
        - `claim_token` string, nullable: Single-use claim token (shown once); `null` if the account is already claimed, or if a claim link issued earlier is still valid (see `claim_link_already_issued`).
        - `claim_url` string, uri, nullable: Ready-to-use claim link for the account holder, built by BeeL for the current environment (no need to assemble `/reclamar?token=` yourself). `null` when `claim_token` is `null`.
        - `claim_link_already_issued` boolean, required: `true` when this call repeated the provision of an unclaimed account that already has a claim link still valid (pending and not expired). That link is **kept working**: nothing is issued or revoked, `claim_token` and `claim_url` come back `null`, and no email is re-sent even with `send_email: true` (there is no token in clear to send). The link you already handed out is the one the holder uses. To replace it on purpose, call `POST /v1/accounts/{account_id}/claim-tokens`, which revokes the previous one. `false` otherwise.
        - `company_id` string, uuid, nullable: The account's company id (a UUID) — send it in the `BeeL-Active-Company` header to operate on this account. Every account is born with one company: with a `tax_profile` it is ready to invoice; without one it has no NIF yet (the holder registers it on claim), and its issuing readiness reports `COMPANY_HAS_NO_NIF` until then. `null` only when resending an `external_ref` whose account now holds two or more companies.
      - `series` AccountImportSeriesOutcome[], required: What each declared series did on this account. Empty when the import declares none.
        - `document_type` 'UNASSIGNED' | 'STANDARD' | 'SIMPLIFIED' | 'CORRECTIVE' | 'SIMPLIFIED_CORRECTIVE' | 'PROFORMA', required: Document type associated with a series. Values mirror `InvoiceType`, so the series a document needs is named exactly like the document: - UNASSIGNED: Legacy value of series created before types existed. A series numbers only documents of its own type, so an `UNASSIGNED` series numbers none (`422 SERIES_INCOMPATIBLE_DOC_TYPE`); give it a type to keep using it. No series can be created with it or moved to it (`422 SERIES_UNASSIGNED_TYPE_NOT_ALLOWED`). The live ones were given the type they numbered most. - STANDARD: Standard invoice - SIMPLIFIED: Simplified invoice - CORRECTIVE: Corrects or cancels a previous standard invoice (R1–R4) - SIMPLIFIED_CORRECTIVE: Corrects a simplified invoice (R5). Spanish law requires R5 to be numbered apart from R1–R4, so they have a series type of their own - PROFORMA: Proforma (commercial document, non-fiscal numbering)
        - `action` 'ALREADY_EXISTS' | 'CREATED' | 'MANUAL_REVIEW_REQUIRED', required: * `ALREADY_EXISTS` — the account already numbers like this. Nothing was touched. A series counts as present when it numbers identically, whether it is written with `{CODIGO}` or with the code spelled out. * `CREATED` — the series was created (in a preview: would be). * `MANUAL_REVIEW_REQUIRED` — neither the declared code nor its `<code>2` fallback is free, or the resolved format falls outside the series grammar. Inventing a third code would be guessing, and changing the format of a series that has already numbered a document is not something an import may do. Resolve it on the account and re-upload.
        - `code` string, nullable: Code the series was created under — not necessarily the declared one, when that was already taken by a different numbering.
        - `format` string, nullable: Format with `{REF}` already resolved to this row's `external_ref`.
      - `customers` AccountImportCustomersOutcome: What the shared customers file did on one account. `already_existed` is the number that makes re-uploading cheap: a customer whose tax id the account already knows is counted, not duplicated.
        - `created` integer, required
        - `already_existed` integer, required
        - `failed` integer, required
      - `errors` AccountImportIssue[], required: What stops this row. A row with errors is never executed.
        - `code` 'REQUIRED_FIELD_MISSING' | 'EXTERNAL_REF_DUPLICATED' | 'ENTITY_TYPE_UNKNOWN' | 'NIF_INVALID_FORMAT' | 'NIF_NOT_IN_CENSUS' | 'EMAIL_INVALID' | 'POSTAL_CODE_INVALID' | 'POSTAL_CODE_PADDED' | 'IBAN_INVALID' | 'IRPF_NOT_NUMERIC' | 'IRPF_NOT_ALLOWED' | 'LANGUAGE_UNSUPPORTED' | 'ACCOUNT_ALREADY_EXISTS' | 'ACCESS_LEVEL_INSUFFICIENT' | 'LIVE_ACTIVATION_NOT_ENTITLED' | 'NIF_ALREADY_LIVE_ELSEWHERE' | 'NIF_LIMIT_REACHED' | 'PROVISIONING_EMAIL_ALREADY_REGISTERED' | 'PROVISIONING_ACCOUNT_CLAIMED' | 'SERIES_CODE_TAKEN' | 'SERIES_CODE_DUPLICATED' | 'SERIES_FORMAT_INVALID' | 'SERIES_MANUAL_RESOLUTION_REQUIRED' | 'SERIES_DEFAULT_REPLACED' | 'CUSTOMER_ROW_REJECTED' | 'UNEXPECTED_ROW_FAILURE', required: Machine-readable reason for an issue. New values are added as the import learns to explain more; treat an unknown one as a generic problem rather than failing. Read straight off the row: `REQUIRED_FIELD_MISSING` (the column is there and its **cell** is empty — a missing column is a whole-file error, not a row one), `EXTERNAL_REF_DUPLICATED`, `ENTITY_TYPE_UNKNOWN`, `NIF_INVALID_FORMAT`, `EMAIL_INVALID`, `POSTAL_CODE_INVALID`, `IBAN_INVALID`, `IRPF_NOT_NUMERIC`, `LANGUAGE_UNSUPPORTED`. `IRPF_NOT_ALLOWED` — the rate is a number but not one of `IrpfPercentage`, or not one the row's NIF can bear: the same check as its invoices (see `WithholdingOptions`), so a company (`B…`) declaring 15 % is rejected here. It is an **error**, not a warning: letting it through buys a green preview and an account that fails on its first invoice months later, when nobody connects the two. `POSTAL_CODE_PADDED` — a warning: the postal code was padded with leading zeros, which is almost always the right repair but does change the value, and the first digit picks the province. Census: `NIF_NOT_IN_CENSUS` — the tax id is not in the AEAT register or is no longer active. For an **individual** it also covers a legal name that does not match the one on file; for a **legal entity** the name is not checked at all, because the AEAT identifies a company by its CIF alone and has no answer that means "this name is not the one". State of the account rather than of the file, and the ones that produce `BLOCKED`: `ACCOUNT_ALREADY_EXISTS` (a warning, not a blocker), `ACCESS_LEVEL_INSUFFICIENT`, `LIVE_ACTIVATION_NOT_ENTITLED`, `NIF_ALREADY_LIVE_ELSEWHERE`, `NIF_LIMIT_REACHED`, `PROVISIONING_EMAIL_ALREADY_REGISTERED`, `PROVISIONING_ACCOUNT_CLAIMED` — each the wire code the equivalent single-account call answers with, so the remedy is the one already documented there. `NIF_LIMIT_REACHED` — switching this row on in Live would exceed the "up to N NIFs" cap of your agreement. The cap is cumulative across the file: rows are counted in order, and only rows that would be switched on in Live take a place, so a row whose NIF already invoices in Live, or that is blocked for another reason, never does. In the preview, and when the import already knew it before writing, `value` is the cap. If the cap fills up while the import is running (another activation took the last place between planning and writing), `value` carries the wire code `NIF_LIMIT_REACHED` instead. Series: `SERIES_CODE_TAKEN`, `SERIES_MANUAL_RESOLUTION_REQUIRED` (the declared code and its `<code>2` fallback are both taken), `SERIES_CODE_DUPLICATED` (this same import declares the code twice), `SERIES_FORMAT_INVALID` (the format, once `{REF}` is resolved, cannot number — no `{NUM}`, or the `external_ref` added characters the grammar rejects) and `SERIES_DEFAULT_REPLACED`. The first four leave the account **without** that series, and say so. Customers file: `CUSTOMER_ROW_REJECTED` — a row of the shared customers file that will not load anywhere. One issue per offending column, each with the message the customer import wrote for it; a row rejected with no column-level detail carries a single one. The census code above is used here too, but only ever on the `nif` column. Anything else: `UNEXPECTED_ROW_FAILURE`, with the detail in `value`.
        - `column` string, nullable: Column of the file involved; `null` when the issue is about the row as a whole.
        - `value` string, nullable: The offending value, already normalised.
        - `message` string, required: Human-readable explanation, in the language of the request. Issues coming from the customers file are the exception: they carry the wording the customer import itself produced, which is Spanish — the same text `POST /v1/companies/{company_id}/customers/imports/preview` returns. Re-labelling them to translate would collapse every error of a row into one repeated sentence, which is worse than one untranslated one.
      - `warnings` AccountImportIssue[], required: What you should know even though the row goes through.
        - `code` 'REQUIRED_FIELD_MISSING' | 'EXTERNAL_REF_DUPLICATED' | 'ENTITY_TYPE_UNKNOWN' | 'NIF_INVALID_FORMAT' | 'NIF_NOT_IN_CENSUS' | 'EMAIL_INVALID' | 'POSTAL_CODE_INVALID' | 'POSTAL_CODE_PADDED' | 'IBAN_INVALID' | 'IRPF_NOT_NUMERIC' | 'IRPF_NOT_ALLOWED' | 'LANGUAGE_UNSUPPORTED' | 'ACCOUNT_ALREADY_EXISTS' | 'ACCESS_LEVEL_INSUFFICIENT' | 'LIVE_ACTIVATION_NOT_ENTITLED' | 'NIF_ALREADY_LIVE_ELSEWHERE' | 'NIF_LIMIT_REACHED' | 'PROVISIONING_EMAIL_ALREADY_REGISTERED' | 'PROVISIONING_ACCOUNT_CLAIMED' | 'SERIES_CODE_TAKEN' | 'SERIES_CODE_DUPLICATED' | 'SERIES_FORMAT_INVALID' | 'SERIES_MANUAL_RESOLUTION_REQUIRED' | 'SERIES_DEFAULT_REPLACED' | 'CUSTOMER_ROW_REJECTED' | 'UNEXPECTED_ROW_FAILURE', required: Machine-readable reason for an issue. New values are added as the import learns to explain more; treat an unknown one as a generic problem rather than failing. Read straight off the row: `REQUIRED_FIELD_MISSING` (the column is there and its **cell** is empty — a missing column is a whole-file error, not a row one), `EXTERNAL_REF_DUPLICATED`, `ENTITY_TYPE_UNKNOWN`, `NIF_INVALID_FORMAT`, `EMAIL_INVALID`, `POSTAL_CODE_INVALID`, `IBAN_INVALID`, `IRPF_NOT_NUMERIC`, `LANGUAGE_UNSUPPORTED`. `IRPF_NOT_ALLOWED` — the rate is a number but not one of `IrpfPercentage`, or not one the row's NIF can bear: the same check as its invoices (see `WithholdingOptions`), so a company (`B…`) declaring 15 % is rejected here. It is an **error**, not a warning: letting it through buys a green preview and an account that fails on its first invoice months later, when nobody connects the two. `POSTAL_CODE_PADDED` — a warning: the postal code was padded with leading zeros, which is almost always the right repair but does change the value, and the first digit picks the province. Census: `NIF_NOT_IN_CENSUS` — the tax id is not in the AEAT register or is no longer active. For an **individual** it also covers a legal name that does not match the one on file; for a **legal entity** the name is not checked at all, because the AEAT identifies a company by its CIF alone and has no answer that means "this name is not the one". State of the account rather than of the file, and the ones that produce `BLOCKED`: `ACCOUNT_ALREADY_EXISTS` (a warning, not a blocker), `ACCESS_LEVEL_INSUFFICIENT`, `LIVE_ACTIVATION_NOT_ENTITLED`, `NIF_ALREADY_LIVE_ELSEWHERE`, `NIF_LIMIT_REACHED`, `PROVISIONING_EMAIL_ALREADY_REGISTERED`, `PROVISIONING_ACCOUNT_CLAIMED` — each the wire code the equivalent single-account call answers with, so the remedy is the one already documented there. `NIF_LIMIT_REACHED` — switching this row on in Live would exceed the "up to N NIFs" cap of your agreement. The cap is cumulative across the file: rows are counted in order, and only rows that would be switched on in Live take a place, so a row whose NIF already invoices in Live, or that is blocked for another reason, never does. In the preview, and when the import already knew it before writing, `value` is the cap. If the cap fills up while the import is running (another activation took the last place between planning and writing), `value` carries the wire code `NIF_LIMIT_REACHED` instead. Series: `SERIES_CODE_TAKEN`, `SERIES_MANUAL_RESOLUTION_REQUIRED` (the declared code and its `<code>2` fallback are both taken), `SERIES_CODE_DUPLICATED` (this same import declares the code twice), `SERIES_FORMAT_INVALID` (the format, once `{REF}` is resolved, cannot number — no `{NUM}`, or the `external_ref` added characters the grammar rejects) and `SERIES_DEFAULT_REPLACED`. The first four leave the account **without** that series, and say so. Customers file: `CUSTOMER_ROW_REJECTED` — a row of the shared customers file that will not load anywhere. One issue per offending column, each with the message the customer import wrote for it; a row rejected with no column-level detail carries a single one. The census code above is used here too, but only ever on the `nif` column. Anything else: `UNEXPECTED_ROW_FAILURE`, with the detail in `value`.
        - `column` string, nullable: Column of the file involved; `null` when the issue is about the row as a whole.
        - `value` string, nullable: The offending value, already normalised.
        - `message` string, required: Human-readable explanation, in the language of the request. Issues coming from the customers file are the exception: they carry the wording the customer import itself produced, which is Spanish — the same text `POST /v1/companies/{company_id}/customers/imports/preview` returns. Re-labelling them to translate would collapse every error of a row into one repeated sentence, which is worse than one untranslated one.
    - `customers_source` AccountImportCustomersSource: The shared customers file, checked once for the whole import — it is the same list for every account, so validating it per account would repeat the same census lookups N times over. Deliberately **not** the shape of `POST /v1/companies/{company_id}/customers/imports/preview`: that one reports duplicates against one company's customers, and here there is no single company to be duplicate of. What is reported here is only what makes a customer unusable everywhere — bad data, or a tax id the AEAT does not know.
      - `total_rows` integer, required
      - `valid` integer, required: Customers that will be applied to every account.
      - `invalid` integer, required: Customers that will be applied to none.
      - `rejected` AccountImportCustomerRow[], required: The rejected rows, so you can fix the file. Valid rows are counted, not listed.
        - `row_number` integer, required: Row as the spreadsheet counts it (header is row 1).
        - `nif` string, nullable
        - `legal_name` string, nullable
        - `errors` AccountImportIssue[], required
          - `code` 'REQUIRED_FIELD_MISSING' | 'EXTERNAL_REF_DUPLICATED' | 'ENTITY_TYPE_UNKNOWN' | 'NIF_INVALID_FORMAT' | 'NIF_NOT_IN_CENSUS' | 'EMAIL_INVALID' | 'POSTAL_CODE_INVALID' | 'POSTAL_CODE_PADDED' | 'IBAN_INVALID' | 'IRPF_NOT_NUMERIC' | 'IRPF_NOT_ALLOWED' | 'LANGUAGE_UNSUPPORTED' | 'ACCOUNT_ALREADY_EXISTS' | 'ACCESS_LEVEL_INSUFFICIENT' | 'LIVE_ACTIVATION_NOT_ENTITLED' | 'NIF_ALREADY_LIVE_ELSEWHERE' | 'NIF_LIMIT_REACHED' | 'PROVISIONING_EMAIL_ALREADY_REGISTERED' | 'PROVISIONING_ACCOUNT_CLAIMED' | 'SERIES_CODE_TAKEN' | 'SERIES_CODE_DUPLICATED' | 'SERIES_FORMAT_INVALID' | 'SERIES_MANUAL_RESOLUTION_REQUIRED' | 'SERIES_DEFAULT_REPLACED' | 'CUSTOMER_ROW_REJECTED' | 'UNEXPECTED_ROW_FAILURE', required: Machine-readable reason for an issue. New values are added as the import learns to explain more; treat an unknown one as a generic problem rather than failing. Read straight off the row: `REQUIRED_FIELD_MISSING` (the column is there and its **cell** is empty — a missing column is a whole-file error, not a row one), `EXTERNAL_REF_DUPLICATED`, `ENTITY_TYPE_UNKNOWN`, `NIF_INVALID_FORMAT`, `EMAIL_INVALID`, `POSTAL_CODE_INVALID`, `IBAN_INVALID`, `IRPF_NOT_NUMERIC`, `LANGUAGE_UNSUPPORTED`. `IRPF_NOT_ALLOWED` — the rate is a number but not one of `IrpfPercentage`, or not one the row's NIF can bear: the same check as its invoices (see `WithholdingOptions`), so a company (`B…`) declaring 15 % is rejected here. It is an **error**, not a warning: letting it through buys a green preview and an account that fails on its first invoice months later, when nobody connects the two. `POSTAL_CODE_PADDED` — a warning: the postal code was padded with leading zeros, which is almost always the right repair but does change the value, and the first digit picks the province. Census: `NIF_NOT_IN_CENSUS` — the tax id is not in the AEAT register or is no longer active. For an **individual** it also covers a legal name that does not match the one on file; for a **legal entity** the name is not checked at all, because the AEAT identifies a company by its CIF alone and has no answer that means "this name is not the one". State of the account rather than of the file, and the ones that produce `BLOCKED`: `ACCOUNT_ALREADY_EXISTS` (a warning, not a blocker), `ACCESS_LEVEL_INSUFFICIENT`, `LIVE_ACTIVATION_NOT_ENTITLED`, `NIF_ALREADY_LIVE_ELSEWHERE`, `NIF_LIMIT_REACHED`, `PROVISIONING_EMAIL_ALREADY_REGISTERED`, `PROVISIONING_ACCOUNT_CLAIMED` — each the wire code the equivalent single-account call answers with, so the remedy is the one already documented there. `NIF_LIMIT_REACHED` — switching this row on in Live would exceed the "up to N NIFs" cap of your agreement. The cap is cumulative across the file: rows are counted in order, and only rows that would be switched on in Live take a place, so a row whose NIF already invoices in Live, or that is blocked for another reason, never does. In the preview, and when the import already knew it before writing, `value` is the cap. If the cap fills up while the import is running (another activation took the last place between planning and writing), `value` carries the wire code `NIF_LIMIT_REACHED` instead. Series: `SERIES_CODE_TAKEN`, `SERIES_MANUAL_RESOLUTION_REQUIRED` (the declared code and its `<code>2` fallback are both taken), `SERIES_CODE_DUPLICATED` (this same import declares the code twice), `SERIES_FORMAT_INVALID` (the format, once `{REF}` is resolved, cannot number — no `{NUM}`, or the `external_ref` added characters the grammar rejects) and `SERIES_DEFAULT_REPLACED`. The first four leave the account **without** that series, and say so. Customers file: `CUSTOMER_ROW_REJECTED` — a row of the shared customers file that will not load anywhere. One issue per offending column, each with the message the customer import wrote for it; a row rejected with no column-level detail carries a single one. The census code above is used here too, but only ever on the `nif` column. Anything else: `UNEXPECTED_ROW_FAILURE`, with the detail in `value`.
          - `column` string, nullable: Column of the file involved; `null` when the issue is about the row as a whole.
          - `value` string, nullable: The offending value, already normalised.
          - `message` string, required: Human-readable explanation, in the language of the request. Issues coming from the customers file are the exception: they carry the wording the customer import itself produced, which is Spanish — the same text `POST /v1/companies/{company_id}/customers/imports/preview` returns. Re-labelling them to translate would collapse every error of a row into one repeated sentence, which is worse than one untranslated one.
    - `own_company_customers` AccountImportCustomersOutcome: What the shared customers file did on one account. `already_existed` is the number that makes re-uploading cheap: a customer whose tax id the account already knows is counted, not duplicated.
      - `created` integer, required
      - `already_existed` integer, required
      - `failed` integer, required
    - `statistics` AccountImportStatistics, required: Aggregated outcome. Every field except the write counters classifies rows of the accounts file: each row falls into exactly one of `valid`, `with_warnings`, `already_existed`, `blocked` and `with_errors`, so those five add up to `total_rows`. The write counters — `accounts_created`, `live_activations_created`, `series_created`, `customers_created` — are the only fields that count writes, so they are what to read to know whether an import worked. They are all `0` in a preview.
      - `total_rows` integer, required
      - `valid` integer, required
      - `with_warnings` integer, required
      - `already_existed` integer, required
      - `blocked` integer, required
      - `with_errors` integer, required
      - `importable` integer, required: `valid + with_warnings` — rows a real import would provision.
      - `accounts_created` integer, required: Accounts actually provisioned by this request. Always `0` in a preview.
      - `live_activations_created` integer, required: NIFs actually switched on by this request. In Live each one adds an item to your subscription, so this is what was billed. Always `0` in a preview.
      - `live_activations_pending` integer, required: NIFs a real import **would** switch on. In a preview this is the billing forecast — read it before importing; in an import it is what is left, normally `0`.
      - `series_created` integer, required
      - `customers_created` integer, nullable: Customer rows created across all **managed accounts** of the file. `null` when no customers file was sent, and `0` in a preview. Your own company, when `options.apply_customers_to_own_company` asked for it, is counted apart in `own_company_customers` and never here.
  - `meta` ResponseMeta
    - `timestamp` string, date-time
    - `request_id` string

## Other responses

- `400`: The file could not be read: `EMPTY_FILE`, `INVALID_FORMAT`, `ENCODING_ERROR`, `PARSING_ERROR`, `HEADER_ROW_NOT_FOUND` (no header row in its first 20 lines; the message names the file, the columns it looked for and the closest row it saw), `DUPLICATE_HEADER` (a column appears twice, so there is no telling which one holds the value) or `NO_DATA_ROWS` (a header row and nothing under it). Nothing was written.
- `401`: Missing or invalid authentication. Like every other error, `message`/`detail` is localized according to `Accept-Language` (`es`, `en`, `ca`); defaults to English when the header is missing or asks for none of those.
- `402`: `PAYMENT_REQUIRED` — your own billing is unpaid (payment retries are exhausted), so nothing can be provisioned on your behalf until the outstanding invoice is settled. Decided before the file is read: no row is reported.
- `403`: `FEATURE_NOT_AVAILABLE` — your account does not hold the managed-accounts capability. A credential without the `accounts:write` scope answers the same way. With `options.apply_customers_to_own_company` there are two more, both decided before the files are read: `ACTIVE_COMPANY_REQUIRED`, when your account holds several NIFs and none is in focus — the option writes on your own company and this endpoint never picks one for you — and `INSUFFICIENT_SCOPE`, when the API key lacks `customers:write`. `MANAGED_ACCESS_INSUFFICIENT` — the access you hold over an existing managed account was lowered below what the import needs while the request was already running. Nothing is written for that account; retry once the access is back to `OPERATE`.
- `409`: `IDEMPOTENCY_KEY_MISMATCH` — this key was already used for a different request. Use a new key, or resend the original request to replay its cached response.
- `413`: Payload too large. On this operation that normally means an uploaded file exceeded its own limit (`FILE_TOO_LARGE`). Any request may also be rejected with `REQUEST_BODY_TOO_LARGE` when the body as a whole exceeds the maximum described under *Request body size*. In both cases the `details` object carries the applicable maximum, except when the HTTP connector cuts the request first, which carries no `details`.
- `422`: `TOO_MANY_RECORDS` — a file carries more rows than its limit. The whole file is rejected and no row is reported: an import of this weight is answered synchronously, so the cap is what keeps the request from timing out half-done. `MISSING_REQUIRED_FIELD` naming `customers_file` — `options.apply_customers_to_own_company` was sent without the customers file it applies. Ignoring the option instead would answer `201` to a request that did none of what it asked for.
- `429`: Rate limit exceeded. `RATE_LIMIT_EXCEEDED` when the request rate of your credential is over its limit; `TOO_MANY_REQUESTS` when an email the operation sends is held back because too many went out to that recipient in a short time. Either way, retry after the `Retry-After` seconds. `QUOTA_EXCEEDED` is the other `429`: the operation would go over a quota of your plan for the current period (issued invoices, for instance). Retrying does not help until the quota resets or the plan changes. `error.details` carries `feature_code`, `current_usage`, `quota_limit` and `reset_date`, and `RateLimit-Reset` the seconds until the reset when it is known.
- `500`: Internal server error
- `default`: Any status code the operation does not list above. Every operation declares it, so a generated client always has a branch to fall into and never loses the cause of a failure it did not anticipate. This is where the transport-level answers land — `405`, `406`, `415` and `429` — together with any status a future version of the API starts returning. All of them carry the same error envelope as the codes listed explicitly, so `error.code` is what tells them apart: switching on the status code alone is not enough. See «Transport-level errors» in the API description for when each one is produced. A `402` carrying `SUBSCRIPTION_REQUIRED` lands here when the account has no active subscription. `error.details` says why and what to do: `current_status` (absent if it never had one), `required_permission` and `action`. A `503` carrying `SERVICE_UNAVAILABLE` lands here too, with a `Retry-After` header: a dependency of BeeL itself (the database, the email provider) was momentarily unavailable and nothing was done. Retry after those seconds. A `502` carrying `EXTERNAL_SERVICE_ERROR` also lands here: an outbound integration the operation depends on failed or did not answer in time. It is a transient condition — retry with the same `Idempotency-Key` where the operation accepts one. One exception to the envelope: a failure of the network edge that never reaches the application (`502`, `503`, `504`, `524`) is generated by Cloudflare and its body is not BeeL's — it may not even be JSON. Treat those as "no answer", and retry.

---

[API](https://skmtc.dev/beel/apis/beel-public-api.md) · [All operations](https://skmtc.dev/beel/apis/beel-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/beel/apis/beel-public-api/revisions/ffbc0ae31f79?raw)
