# Submission states

Every state a VeriFactu submission can be in, what triggers transitions, and when you need to act.

A VeriFactu submission has its own lifecycle, separate from the invoice's commercial state (`DRAFT` → `ISSUED` → `PAID` …). The invoice carries a `verifactu` object on the response that holds the submission state and AEAT response — this page is the canonical map.

## Invoice status (commercial) vs VeriFactu status (fiscal)

These are two different state machines on the same invoice:

```text
COMMERCIAL  (status)              FISCAL  (verifactu.submission_status)
─────────────                     ─────────────────────
DRAFT                              (none — verifactu.enabled = false)
  ↓ issue                            ↓ submission sent
ISSUED          ── submission ──> PENDING
  ↓ send                               ↓ AEAT answers (or the record never gets there)
SENT                              ACCEPTED   or   REJECTED
  ↓ pay
PAID
  ↓ correct / void                   ↓ on void (cancellation record)
RECTIFIED / VOIDED                VOIDED
```

Subscribe to `verifactu.status.updated` to be notified of fiscal-status changes — see [Webhook events](/webhooks/events).

## The fiscal states

The invoice response exposes a nested `verifactu` object. Its `submission_status` field carries the current state. The vocabulary has exactly five values:

| verifactu.submission_status | What it means | Terminal | What to do |
|---|---|---|---|
| **`PENDING`** | Submitted; AEAT has not answered yet. A temporary AEAT server error also stays PENDING while BeeL. retries it. | No — retried automatically | Wait — usually seconds. BeeL. keeps asking AEAT on its own. |
| **`ACCEPTED`** | Accepted by AEAT. With an error_code alongside, it was accepted with errors: registered, but AEAT flagged some data. | Yes | Nothing — unless error_code is set: then review the flagged data and, if it is wrong, issue a corrective. |
| **`VOIDED`** | A cancellation record (registro de anulación) was accepted by AEAT. | Yes | Nothing. |
| **`REJECTED`** | Not in AEAT's registry: AEAT refused it, the submission was refused before reaching AEAT, or BeeL. gave up waiting (including when the retries after a temporary AEAT error ran out). | Yes | Read error_code (AEAT's code, or null when AEAT gave none). Fix the data with a corrective (void only an invoice that should never have been issued); for a cause outside the invoice, contact BeeL. |
| **`NOT_SUBMITTED`** | Issued with VeriFactu enabled but with no live record: AEAT does not know the invoice exists. | No — but not retried | Right after issuing it can still be in flight, so re-read it. If it persists, contact BeeL. with the invoice ID. |

If there is no submission to describe yet, the `submission_status` is absent: drafts, scheduled invoices, proformas, and invoices whose `verifactu.enabled` is `false` (those are outside this axis entirely) — see [Auto-submit policy](/verifactu/auto-submit). An **issued** invoice with `verifactu.enabled: true` always reports a state: if no live record exists — the registration fell through and AEAT does not know the invoice exists — it reports `NOT_SUBMITTED`, which is transient right after issuing while the submission is still in flight.

<Callout type="info">
  **The list filter uses the very same vocabulary.** `GET /invoices?verifactu_status=...` accepts `PENDING`, `ACCEPTED`, `VOIDED`, `REJECTED` and `NOT_SUBMITTED` — a value read from an invoice can be fed straight back into the filter. Only invoices with VeriFactu enabled can match; to select the ones outside the axis (no VeriFactu record at all) use `?verifactu_enabled=false` instead. There is no `NO_VERIFACTU` value: sending it returns `422`.
</Callout>

### What moves on its own, and what does not

While a record is `PENDING`, BeeL. keeps asking AEAT for the outcome, on a backoff. A temporary AEAT server error keeps the record `PENDING`, without `error_code` or `error_message`, while BeeL. retries it; it becomes `REJECTED` only if the retries run out. Once AEAT gives a real verdict, BeeL. stops asking, and **nothing resubmits the record for you**. What to do then is in [Handling AEAT rejections](/verifactu/handling-rejections).

### Is `ACCEPTED` final?

**Yes.** Once a record reads `ACCEPTED` — with or without an `error_code` — BeeL. does not ask AEAT about it again, and nothing in BeeL. can move it back to `PENDING` or on to `REJECTED`. There is no late rejection to wait for, so there is no later `verifactu.status.updated` turning an accepted invoice into a rejected one.

What can still happen to an accepted invoice is something **you** do, and it arrives as its own record with its own status:

- you **void** it: a cancellation record is sent, and the invoice's `submission_status` becomes `VOIDED` once AEAT accepts it — see [Voiding](#voiding);
- you issue a **corrective**: the corrective has its own registration, and the original stays `ACCEPTED`.

What this means for reconciliation: you never need to re-check accepted invoices. The window worth sweeping is the one where a state can still change or still needs you — invoices `PENDING`, `REJECTED` or `NOT_SUBMITTED`. A sweep over the last few days, run daily, covers an outage of your own webhook endpoint; see [Reconcile, do not only listen](/verifactu/handling-rejections#reconcile-do-not-only-listen) and pace it with [Rate limits](/guides/rate-limits#pacing-a-polling-or-reconciliation-job).

> **Rules that apply here:** [REC-008 · Follow submission_status and fix what AEAT rejects](/rules/records#rec-008)

## Reading the AEAT response

Every submission stores the AEAT response on the invoice resource under the `verifactu` object. The relevant fields:

```json
{
  "id": "...",
  "invoice_number": "INV-2026/00042",
  "status": "ISSUED",
  "verifactu": {
    "enabled": true,
    "submission_status": "ACCEPTED",
    "registration_number": "094365f5-9637-40e9-851a-4e964a939b0b",
    "registered_at": "2026-05-18T10:24:36Z",
    "invoice_hash": "1863E77D540083E4259F4AF44322E16B962CB5D72C2F949AB2B30FA83E93FF84",
    "qr_url": "https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR?nif=B27534239&numserie=INV-2026%2F00042&fecha=18-05-2026&importe=121.00",
    "qr_base64": "iVBORw0KGgoAAAANSUhEUgAAAMgAAADI...",
    "skip_reason": null
  }
}
```

The block is part of the invoice resource, so
[its fields are documented with it](/invoices/getCompanyInvoice). Several of them are routinely
misread:

- **`enabled: false` is not a failure.** It means VeriFactu never applied to this
  invoice: it was issued by a NIF outside the VeriFactu regime. There is no
  `submission_status` to inspect in that case.
- **`registration_number` is a BeeL. identifier**, not a code issued by AEAT. It
  identifies the submitted record, and stays `null` when the record never got that far.
- **`registered_at` is when the record was submitted**, not when AEAT accepted it.
- **`invoice_hash`, `qr_url` and `qr_base64` are there from `PENDING`**, and their
  presence does not mean AEAT accepted anything — see
  [QR code and the invoice PDF](/verifactu/qr-and-pdf#the-qr-exists-from-pending).
- **`invoice_hash` fingerprints *this* invoice, not the chain.** It is the hash of this
  record as submitted. The link to the previous record is kept on the submission
  record and is **not published on the invoice resource**, so chain integrity is not
  something you can verify from the API.
- **`error_code` / `error_message` are single values, not arrays**, and they are also
  populated on submissions AEAT *accepted with errors* — their presence does not by
  itself mean the invoice was rejected. Read `submission_status` first. `error_code`
  is AEAT's code when AEAT gave one; `error_message` is **human-readable text only** —
  its wording and language are not stable, so show it, log it, never parse it. When
  there is nothing to report, both are absent from the block rather than `null`.
- **Right after issuing, the block is almost empty.** The issue response, and a read in
  the next few seconds, carry only `enabled` and `submission_status: NOT_SUBMITTED`. The
  hash, QR, `registration_number` and `registered_at` appear together with `PENDING`.

## What triggers each transition

| Event | Effect on `verifactu.submission_status` |
|---|---|
| You issue an invoice from a NIF under the regime | `NOT_SUBMITTED` for a few seconds, while the record is built → `PENDING` |
| AEAT accepts the record | `PENDING` → `ACCEPTED` (with an `error_code` if accepted with errors) |
| AEAT rejects the record | `PENDING` → `REJECTED`, with `error_code` and `error_message` |
| AEAT answers with a temporary server-side error | Stays `PENDING`, with no `error_code` or `error_message`, while BeeL. retries; → `REJECTED` only if the retries run out |
| The submission is refused before it reaches AEAT | `NOT_SUBMITTED` → `REJECTED`, no `error_code`, an explanatory `error_message` |
| AEAT already holds a different invoice with the same number and issue date | `NOT_SUBMITTED` → `REJECTED`, no `error_code`, an `error_message` that says the number is taken ([what to do](/verifactu/handling-rejections#the-number-is-already-taken-at-aeat)) |
| BeeL. stops waiting for an answer that never comes | `PENDING` → `REJECTED`; `error_message` says the submission was abandoned |
| You issue a corrective on an accepted invoice | New corrective enters its own `PENDING` cycle; the original stays `ACCEPTED` (the commercial `status` flips to `RECTIFIED` / `VOIDED`) |
| You void an invoice whose registration AEAT accepted | A cancellation record is sent; see [Voiding](#voiding) below |

The `verifactu` block summarises the registration, so after a void its hash, number and date are still the registration record's. Each record — registration and cancellation — carries its own `submission_status` in [List the VeriFactu records of an invoice](/invoices/listCompanyInvoiceVerifactuRecords); see [Did AEAT accept the cancellation?](/verifactu/cancel-and-fix#did-aeat-accept-the-cancellation).

<Callout type="info">
  **`REJECTED` is not one situation.** It means the invoice is not in AEAT's registry right now — because AEAT refused it, because the submission was refused before reaching AEAT, or because BeeL. gave up waiting. An `error_code` means AEAT gave a coded answer; no code means it did not — either the record never reached AEAT, or it was abandoned while waiting. How to tell them apart and what to do with each is in [Handling AEAT rejections](/verifactu/handling-rejections).
</Callout>

### Voiding

The `verifactu` block summarises the registration, so after a void its hash, number and date are still the registration record's. What `submission_status` shows depends on the cancellation:

| Situation | `status` | `verifactu.submission_status` |
|---|---|---|
| Cancellation sent, AEAT has not answered yet | `VOIDED` | Unchanged — still the registration's (typically `ACCEPTED`). It never goes back to `PENDING` |
| AEAT accepted the cancellation | `VOIDED` | `VOIDED` |
| AEAT rejected the cancellation | `VOIDED` | `REJECTED`, with the cancellation's `error_code` / `error_message` |
| You voided while the registration was still `PENDING` | `VOIDED` | The registration's status. The cancellation is sent only after AEAT accepts the registration |
| The registration never reached AEAT, or AEAT rejected it | `VOIDED` | Unchanged (`REJECTED`), or absent if the invoice never had a record (`NOT_SUBMITTED` before the void). **Nothing is sent to AEAT** — there is no record there to cancel |

The invoice's own `status` flips to `VOIDED` immediately in every case; see [Did AEAT accept the cancellation?](/verifactu/cancel-and-fix#did-aeat-accept-the-cancellation).

## Recovering from a rejection

There is no public endpoint to retry a submission. Wait on a temporary AEAT error, fix the invoice's own data with a new fiscal document, and ask support for anything outside the invoice — each case, per AEAT code, in [Handling AEAT rejections](/verifactu/handling-rejections).

## AEAT error codes

`error_code` carries the code AEAT returned, unchanged. The codes you are most likely to meet — grouped by what you should do about each — are listed in [Handling AEAT rejections](/verifactu/handling-rejections#aeat-codes-by-what-to-do). The complete list is in the [AEAT VeriFactu documentation](https://sede.agenciatributaria.gob.es/Sede/iva/sistemas-emision-facturas/sistemas-informaticos-facturacion-sif-veri-factu.html).

## Webhooks for fiscal status changes

Subscribe to `verifactu.status.updated` to be notified each time the public status changes:

```json
{
  "type": "verifactu.status.updated",
  "data": {
    "invoice_id": "...",
    "invoice_number": "INV-2026/00042",
    "verifactu_registration_id": "...",
    "previous_status": "PENDING",
    "new_status": "ACCEPTED",
    "qr_url": "https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR?nif=B27534239&numserie=INV-2026%2F00042&fecha=18-05-2026&importe=121.00",
    "qr_base64": "iVBORw0KGgoAAAANSUhEUgAAAMgAAADI...",
    "invoice_hash": "1863E77D540083E4259F4AF44322E16B962CB5D72C2F949AB2B30FA83E93FF84"
  }
}
```

<Callout type="info">
  **Four of the five values travel by webhook.** `previous_status` and `new_status` carry `PENDING`, `ACCEPTED`, `VOIDED` or `REJECTED`. `NOT_SUBMITTED` never does: it describes an invoice with no record at all, so there is no record whose status could change. The first notification for a record has no `previous_status`.
</Callout>

<Callout type="info">
  **A webhook is sent when the public status changes, and only then.** A submission refused before it reached AEAT, or one BeeL. stopped waiting for, arrives as `REJECTED` like any other rejection. A temporary AEAT error keeps the record `PENDING`, so it sends nothing. Your endpoint can still miss deliveries, so reconcile periodically by listing with `?verifactu_status=REJECTED` and `?verifactu_status=NOT_SUBMITTED`; see [Reconciliation](/verifactu/handling-rejections#reconcile-do-not-only-listen).
</Callout>

See [Webhook events](/webhook-events/onVeriFactuStatusUpdated) for the full payload.

## Related

<Related>

- [Auto-submit policy](/verifactu/auto-submit) — when BeeL. submits in the first place
- [Handling AEAT rejections](/verifactu/handling-rejections) — reading `REJECTED`, per-code actions, reconciliation
- [QR code and the invoice PDF](/verifactu/qr-and-pdf) — when the QR and the final PDF exist
- [What AEAT receives](/verifactu/what-aeat-receives) — why the registered total can differ from `total`
- [Cancel vs amend](/verifactu/cancel-and-fix) — what to do when AEAT rejects
- [Corrective invoices](/verifactu/corrective-invoices) — issuing R1–R5

</Related>

---

Full OpenAPI spec: https://docs.beel.es/api/openapi