NewStripe: a buyer with a NIF always gets a standard invoice, and the simplified threshold is deprecated
BeeL
Get startedMulti-NIFVeriFactuRulesStripeAPI referenceChangelog

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:

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.

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_statusWhat it meansTerminalWhat to do
PENDINGSubmitted; AEAT has not answered yet. A temporary AEAT server error also stays PENDING while BeeL. retries it.No — retried automaticallyWait — usually seconds. BeeL. keeps asking AEAT on its own.
ACCEPTEDAccepted by AEAT. With an error_code alongside, it was accepted with errors: registered, but AEAT flagged some data.YesNothing — unless error_code is set: then review the flagged data and, if it is wrong, issue a corrective.
VOIDEDA cancellation record (registro de anulación) was accepted by AEAT.YesNothing.
REJECTEDNot 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).YesRead 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_SUBMITTEDIssued with VeriFactu enabled but with no live record: AEAT does not know the invoice exists.No — but not retriedRight 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. 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.

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.

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.

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;
  • 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 and pace it with Rate limits.

Reading the AEAT response

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

{
  "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. 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.
  • 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

EventEffect on verifactu.submission_status
You issue an invoice from a NIF under the regimeNOT_SUBMITTED for a few seconds, while the record is built → PENDING
AEAT accepts the recordPENDING → ACCEPTED (with an error_code if accepted with errors)
AEAT rejects the recordPENDING → REJECTED, with error_code and error_message
AEAT answers with a temporary server-side errorStays 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 AEATNOT_SUBMITTED → REJECTED, no error_code, an explanatory error_message
AEAT already holds a different invoice with the same number and issue dateNOT_SUBMITTED → REJECTED, no error_code, an error_message that says the number is taken (what to do)
BeeL. stops waiting for an answer that never comesPENDING → REJECTED; error_message says the submission was abandoned
You issue a corrective on an accepted invoiceNew corrective enters its own PENDING cycle; the original stays ACCEPTED (the commercial status flips to RECTIFIED / VOIDED)
You void an invoice whose registration AEAT acceptedA cancellation record is sent; see 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; see Did AEAT accept the cancellation?.

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.

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:

Situationstatusverifactu.submission_status
Cancellation sent, AEAT has not answered yetVOIDEDUnchanged — still the registration's (typically ACCEPTED). It never goes back to PENDING
AEAT accepted the cancellationVOIDEDVOIDED
AEAT rejected the cancellationVOIDEDREJECTED, with the cancellation's error_code / error_message
You voided while the registration was still PENDINGVOIDEDThe registration's status. The cancellation is sent only after AEAT accepts the registration
The registration never reached AEAT, or AEAT rejected itVOIDEDUnchanged (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?.

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.

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. The complete list is in the AEAT VeriFactu documentation.

Webhooks for fiscal status changes

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

{
  "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"
  }
}

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.

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.

See Webhook events for the full payload.