---
title: "Validate a NIF against the AEAT register"
method: POST
path: "/v1/nif/validate"
tags: ["NIF"]
---

# Validate a NIF against the AEAT register

`POST /v1/nif/validate`

## Request

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

Checks a NIF or CIF against the AEAT register through VeriFactu and returns what the
register says about it. It only reads the register: it creates nothing and stores no
customer.

- **`status`:** distinguishes a NIF found in the register from one that is syntactically
  correct but absent, and from a check that could not be completed because VeriFactu was
  unavailable — in which case the NIF is validated automatically once the service is back.
- **`valid: true`:** means different things by holder. For an individual, AEAT matched NIF
  and name together. For a legal entity the name you sent is **not verified** at all —
  AEAT identifies a company by its CIF alone — so it says nothing about your name.
- **`legal_name_verified`:** tells those two cases apart.
- **`census_status`:** says whether an identified NIF is also deregistered or revoked.

## Invalid input

- **Bad syntax is an answer, not an error:** it comes back `200` with `status: INVALID`, so
  a pre-validation flow never has to tell rejections apart by status code.
- **A missing NIF is an error:** an absent or empty `nif` answers `422` `FIELD_BLANK`, with
  `details.field` naming it.

## Request body

- ValidateNifRequest
  - `nif` string, required: NIF/CIF to validate against the AEAT census. Must be 9 alphanumeric characters.
  - `legal_name` string, nullable: Last name and first name (individual) or business name (legal entity). **Important**: - REQUIRED for individuals (NIFs starting with a number) - OPTIONAL for legal entities (NIFs starting with a letter) For an **individual**, AEAT matches NIF and name together: send a name the census does not recognise and the person is not identified, so the status comes back `INVALID`. For a **legal entity**, the name is **not verified**. AEAT identifies a company by its CIF alone — there is no census answer meaning "this name is wrong", so validation depends only on the CIF and any name you send is accepted. Use the `legal_name` of the response to contrast your own.

## Response `200`

Validation result

- 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 payload. An object for a single resource; an object holding the named collection (and its `pagination`) for a listing. Never a bare array at this level in v1.
    - `valid` boolean, required: true if the NIF is in the AEAT census, **is active**, and — for an individual — the name matches (status = VALID). false otherwise. For a legal entity the name is **not verified**: AEAT identifies a company by its CIF alone, so `valid: true` says nothing about the name you sent. See `legal_name_verified`. Being in the census is not enough: a deregistered or revoked NIF comes back `false`. Read `census_status` to tell those apart from a NIF that is simply not registered, because they are not fixed the same way. A **deregistered** NIF cannot issue invoices, but it can still receive them — a company closing down does not stop you from invoicing what accrued before it closed, and BeeL still accepts it as a recipient. A **revoked** NIF cannot operate at all: it is rejected on both sides.
    - `status` 'PENDING' | 'VALID' | 'INVALID' | 'ERROR', required: Validation status
    - `legal_name` string, nullable: Legal name as it appears in the AEAT census — never an echo of the name you sent. Only available if status = VALID. For a legal entity this is the only signal that your own name is wrong, since the one you send is not checked: comparing the two is up to you. Deliberately withheld when an individual is not identified because the name you sent is close but wrong (`census_status` = `NOT_IDENTIFIED_SIMILAR`). AEAT does return the real name there, but you have just shown you do not know it.
    - `legal_name_verified` boolean: Whether AEAT actually cross-checked the `legal_name` you sent. `true` only for an **individual** that was identified: AEAT matches NIF and name together there, so an identification is the check having passed. Always `false` for a **legal entity** — its name is **not verified**. AEAT identifies a company by its CIF alone and returns the census name without looking at the one you sent, so `valid: true` says nothing about your name. Comparing it against `legal_name` is up to you. Also `false` whenever the census was not reached at all (`PENDING`, `ERROR`, or a syntactically invalid NIF).
    - `census_status` 'IDENTIFIED' | 'IDENTIFIED_DEREGISTERED' | 'IDENTIFIED_REVOKED' | 'NOT_IDENTIFIED' | 'NOT_IDENTIFIED_SIMILAR' | 'UNKNOWN': Census situation AEAT reported for this NIF. Absent when the census was not reached (`PENDING`, `ERROR`, or a syntactically invalid NIF) — absent is not "not registered".
    - `message` string, required: Descriptive message of the validation result, in the language of the request (`Accept-Language`: `es`, `en` or `ca`). Branch on `status` and `census_status`, not on this text. For a legal entity the name is not verified: AEAT identifies a company by its CIF alone. Examples (English): - VALID: "NIF successfully validated against the AEAT census" - INVALID: "The NIF is not in the AEAT census or, for an individual, the name does not match" - PENDING: "NIF validation is pending. The system will validate it automatically when the service is available." - ERROR: "The NIF could not be validated because of a technical error. Try again later."
    - `validated_at` string, date-time, nullable: Timestamp of when the NIF was successfully validated. Only available if status = VALID.
  - `meta` ResponseMeta
    - `timestamp` string, date-time
    - `request_id` string

## Other responses

- `400`: Bad request. From a dashboard session, a `BeeL-Environment` header whose value is not a known environment answers `ENVIRONMENT_HEADER_INVALID`, with the header named in `error.details` (`{"BeeL-Environment": "invalid"}`); it is never read as production. `BUSINESS_RULE_VIOLATION` is the generic answer to an operation the resource does not admit in its current state when that state has no code of its own. It carries no `details`; read the resource again before retrying.
- `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.
- `403`: Authenticated but not allowed. Fourteen causes shared by every operation, told apart by `error.code`; an operation that adds a cause of its own names it in its `403`. You can branch on them exhaustively. - `INSUFFICIENT_SCOPE` — the credential lacks a scope the operation requires; `error.details.missing_scopes` names them as a single comma-separated string (for example `"invoices:write,customers:read"`), not as an array; `required_scopes` has the same shape and lists every scope the operation needs. Retrying will not help: mint a key that holds them. - `COMPANY_READ_ONLY` — the scope is there, but your access level over that NIF only lets you read it. - `ACCOUNT_MANAGEMENT_FORBIDDEN` — the scope is there, but your role over the account, or the management relationship you hold over it, does not cover this operation. - `ACCOUNT_NOT_ACCESSIBLE` — the account is not yours to reach, which is also the answer when it does not exist, so existence is never disclosed. - `ACTIVE_COMPANY_NOT_ACCESSIBLE` — the same, for a NIF: the company in the path, or the one named by `BeeL-Active-Company`, is not one this credential may reach — and again, this is also the answer when it does not exist. - `COMPANY_ACCESS_REVOKED` — your access to the NIF was withdrawn while the request was already in flight, so the write was rejected and nothing was recorded. Retrying will not help until the access is granted again. - `LIVE_CREDENTIAL_REQUIRED` — the operation changes the real account and the call came from a test API key (`beel_sk_test_…`). Use your live key or the dashboard. - `FEATURE_NOT_AVAILABLE` — your subscription does not include the entitlement the operation needs; `error.details.feature_code` names it. This gate runs **before** the scope gate, so for such an operation you never see `INSUFFICIENT_SCOPE` first. - `NO_ACTIVE_ACCOUNT` — the credential does not resolve to an account, so no scope can be evaluated against one. Fail-closed, not a permission that can be granted to you. - `OPERATION_REQUIRES_SESSION` — the operation is available only from the web session; no API key and no OAuth2 token can perform it, whatever scopes it holds. - `OPERATION_REQUIRES_API_KEY` — the reverse: the operation needs a scope that only an API key can hold (such as `accounts:write`), so a dashboard session never satisfies it. - `API_KEY_LIVE_NOT_AVAILABLE` — the call came with a live API key (`beel_sk_live_…`) and the account has no Live mode yet. It is never degraded to Test: use your test key, or activate Live first. - `ACCOUNT_REQUIRES_FOCUS_SWITCH` — dashboard session only: you are a member of the account in the path, but it is not the account in focus. Switch focus to it and retry. - `REQUEST_BLOCKED` — the edge security filter stopped the request before the API saw its body, usually because a text field carries something that looks like markup or a script (HTML tags, `<script>`). Review the body and retry.
- `422`: `VALIDATION_ERROR` — the body parses but a value is not acceptable. `details` is a flat map from the offending property's name (in the contract's `snake_case`, nested paths joined with a dot, e.g. `recipient.address.postal_code`) to a message describing what is wrong with it, one entry per property. When the rejected value is one outside an enum's vocabulary, `details` instead follows `FieldDeserializationError` (`field`, `invalid_value`, `allowed_values`). Two more codes share this status for a value that passes the contract but does not fit where it is stored: `VALUE_TOO_LONG` (a text longer than its column holds) and `VALUE_OUT_OF_RANGE` (a number outside what its column holds). Nothing was written.
- `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)
