VeriFactu submission states
The lifecycle of a VeriFactu billing record — pending, submitted, accepted, rejected, error — what csv and huella mean, how retries and blocked records work, and when to retry, release or subsanar.
Every invoice your company issues under VeriFactu produces a billing record: a
signed XML declaration that is transmitted to the AEAT and cryptographically
chained to the previous record of the same company. The invoice and its record
are two different objects with two different lifecycles — an invoice can be
sent and paid while its record is still rejected by the AEAT.
This page is about the record. If you integrate against Factuarea and you only watch invoice status, you will not notice that the tax authority refused a declaration.
When this applies
The record lifecycle applies to every company that has VeriFactu effectively
enabled, from the moment an invoice leaves draft. It does not apply to:
- Companies still in
no_verifactumode: records are still created and chained locally, but never transmitted, so they stay outside the accepted/rejected cycle (BR-VFC-018, RD 1007/2023 art. 16). - Historical invoices imported with the VeriFactu step skipped — no record is
created at all, so there is nothing to poll (
BR-VFC-009).
Read VeriFactu auto-submission for the gates that decide whether a record is created in the first place.
The five states
status | Meaning | Terminal? | What you do |
|---|---|---|---|
pending | The record exists and is chained, but has not been transmitted yet. | No | Nothing. Transmission is queued. |
submitted | Sent to the AEAT, awaiting the definitive answer. | No | Nothing. Poll. |
accepted | The AEAT registered the declaration. aeat_csv is populated. | Yes — immutable | Nothing. A data error in it is corrected with a new record (subsanar) or, when the invoicing rules require it, a corrective. |
rejected | The AEAT refused it because of a data error (unknown recipient tax ID, schema, totals), or Factuarea stopped it before sending because it broke the AEAT schema (SCHEMA_INVALID). | Definitive answer, but repairable | Fix the data, then subsanar. |
error | Technical transmission failure (timeout, AEAT unreachable), or the record is blocked waiting for something only you can provide (a certificate, a representation). | No | Nothing for a technical failure (it retries by itself); for a blocked record, fix the cause and retry-blocked. |
The distinction that matters is rejected versus error. rejected is the AEAT
saying "I read your declaration and it is wrong". error is the declaration
never arriving. They are repaired by different operations, and confusing them is
the most common integration mistake on this endpoint.
accepted is the only genuinely immutable state: the transition matrix refuses
every transition out of it, because RD 1007/2023 makes a registered record
unalterable. Correcting an accepted record never edits it: subsanar generates a
new record that follows it. Every other state can transition again, which is
what makes retry possible.
What the API sends
You never create a record with a payload — it is created for you. What you do is
read it. The v1 record object is returned by
GET /v1/companies/{company}/verifactu/records/{id},
GET /v1/companies/{company}/verifactu/records
and, keyed by invoice, by
GET /v1/companies/{company}/invoices/{id}/verifactu:
| Field | Meaning |
|---|---|
status | One of the five states above. |
type | ALTA (the invoice was issued) or ANULACION (it was annulled). |
invoice_type | The AEAT invoice type frozen at issue time: F1, F2, F3, R1–R5. |
huella | The SHA-256 fingerprint of this record, uppercase hex. It is the chain link the next record will point at. |
is_blocked | true when the record is in error with no automatic retry: it needs your action and holds back the records after it. |
block_reason | Why it is blocked: MISSING_CERTIFICATE, MISSING_REPRESENTATION, PRESENTER_NOT_ENABLED or SUBMISSION_REJECTED; null otherwise. |
aeat_error_code | The last AEAT code (for example 1110 or 4112), an internal one (MISSING_CERTIFICATE, SCHEMA_INVALID…) or the warning code of a record accepted with errors; null when there is none. |
can_subsanar | Whether POST …/subsanar admits this record: an alta that is accepted, accepted with errors or rejected, is the last one of its invoice and the invoice is not annulled. |
aeat_csv | The Código Seguro de Verificación the AEAT returns on acceptance. null until then. This is the value you reconcile against the tax authority. |
aeat_submission_id | Our transmission identifier, for support conversations. |
transmitted_at | ISO 8601 of the last transmission that reached the AEAT. Populated in submitted and accepted; null in pending, rejected and error. |
environment | The AEAT environment for this company — production or the AEAT test bed. A CSV obtained in the test bed is not a real registration. |
is_simplificada / is_substitute_for_simplified | Whether the source invoice was an F2, and whether this record substitutes simplified invoices with an F3. |
Two fields deserve their own warning.
huella is identity, not a checksum you may recompute. It is calculated
from the issuer tax ID, series and number, issue date, invoice type, total tax
amount, total amount, the previous record's fingerprint and the generation
timestamp — in that exact order and format. If any of those change, the chain
breaks and the whole company's integrity proof fails
(BR-VFC-013). This is why some corrections cannot be repaired
in place; see Retry or subsanar.
aeat_csv is written once. On acceptance it is persisted and never
overwritten, even if the AEAT returns the same CSV on a later transmission. A
record that goes rejected after having been submitted keeps the previous CSV
for audit purposes, so a non-null aeat_csv on a rejected record is expected,
not a bug (BR-VFC-016).
curl https://api.factuarea.com/v1/companies/{company}/invoices/0197a2a8-4cf0-7a31-9a5e-3f2b8c1d6e42/verifactu \
-H "Authorization: Bearer fact_live_3pXnR2VbY7TcA9eFmN5z8KqW"{
"data": {
"id": "0197b3c9-1de2-7c40-8b71-a4d5e6f70123",
"object": "verifactu_record",
"type": "ALTA",
"invoice_type": "F1",
"invoice_number": "F-2026-0042",
"date": "2026-05-15",
"amount": 121.0,
"status": "accepted",
"huella": "9F2C1A0B7E4D6835A1C0B9E8D7F6A5B43C2D1E0F9A8B7C6D5E4F3A2B1C0D9E8F",
"aeat_submission_id": "sub_0197b3c9",
"aeat_csv": "FCT-2026-A1B2C3D4-E5F6",
"environment": "production",
"transmitted_at": "2026-05-15T09:41:02Z",
"is_simplificada": false,
"is_substitute_for_simplified": false,
"created_at": "2026-05-15T09:40:58Z"
}
}Retries have no cap, and some records are blocked
Behind error there are two very different situations.
A technical failure retries by itself, without a limit. If a submission gets no
answer, a server fault or a network failure, its records are retried with growing
intervals — 1, 5, 15 and 30 minutes and then every 55 — so that no more than one
hour passes between two attempts, for as long as the incident lasts (art. 16.4 of
Order HAC/1177/2024). There is no maximum number of attempts, so
max_retries_exceeded is no longer returned for billing records (it still applies to
the retry of an event in no_verifactu mode). While there is an incident, the
next submission carries the incident flag, and records generated meanwhile wait
behind the earlier ones.
A blocked record does not retry, because retrying would fail again until you act:
no usable certificate, no active representation, a presenter that the AEAT has not
enabled or a client-side rejection. It shows is_blocked: true and its
block_reason, and it holds back every record after it in the chain of the
company. Fix the cause and it is released by itself (uploading the certificate,
registering the representation or changing the mode reactivates what that event can
free) or in one call to
POST /v1/companies/{company}/verifactu/records/retry-blocked,
which answers 202 with how many records it reactivated. The causes and their
meaning are in VeriFactu auto-submission.
GET /v1/companies/{company}/verifactu/stats reports how many records are still waiting
(pending_incident_count), how many of them are blocked (blocked_incident_count)
and the age of the oldest (oldest_pending_at).
The attempt counter and the schedule of the next retry are still not exposed in the
record object; the state is what you poll. The audit timeline is in
GET /v1/companies/{company}/verifactu/records/{id}/activities.
Retry or subsanar
Both operations act on a record that already exists, and a third one releases the blocked ones. They are not interchangeable.
| Record state | Cause | Operation | Why |
|---|---|---|---|
error, not blocked | The declaration never reached the AEAT. | POST /v1/companies/{company}/verifactu/records/{id}/retry | The stored XML is correct. It goes out again in the next batch of the company, in order. |
error, blocked | Something needs you: certificate, representation, presenter. | Fix the cause, then POST /v1/companies/{company}/verifactu/records/retry-blocked | It reactivates every blocked record of the company at once. |
rejected | The AEAT read it and refused the data, or it never left because it broke the schema. | POST /v1/companies/{company}/verifactu/records/{id}/subsanar | A new record, regenerated from corrected master data. |
accepted | A data error that the invoicing rules do not require a corrective for. | POST /v1/companies/{company}/verifactu/records/{id}/subsanar | A new record follows the accepted one, which stays untouched. |
accepted, and the invoicing rules require it | A wrong amount, a return, an error that must be corrected with a corrective. | Issue a corrective invoice. | That is the route the invoicing rules prescribe. |
rejected or accepted, but the fix touches a fingerprint field | The wrong total, date, number, tax ID or invoice type was declared. | Neither — annul and re-issue. | Changing a fingerprint field would invalidate the chain. subsanar refuses it up front. |
Retrying blind has no attempt limit either. Subsanación is a deliberate manual
correction with new data, the regulation sets no cap on it, and it never edits a
record: it generates a new alta, with the same invoice key as the one it corrects,
its own fingerprint and its own position in the chain, and the original stays
untouched (BR-VFC-020, BR-VFC-046). Before
creating anything it compares the regenerated fingerprint-relevant fields against the
stored ones; if any differs, it answers 422 with the subcode that tells you an
annulment is required, and nothing is created. The full flow, the error subcodes and
the prevention advice live in
VeriFactu record subsanación.
What appears on the PDF
The record state does not change the PDF. Whatever the state, the invoice
prints the same legal QR block in the top-right corner of the first page: the
QR tributario: label, a 30×30 mm code addressing the AEAT verification service
with the issuer tax ID, series and number, date and total, and the legend
underneath (BR-VFC-015).
Three consequences worth designing for:
- The QR is printed as soon as a record exists — including while it is
pending,errororrejected. A recipient who scans it before acceptance sees the AEAT reporting no registration. That is correct behaviour, not a defect. - The CSV is not printed on the PDF. It is only available through the API and the dashboard.
- The fingerprint and the registration timestamp stopped being printed as well. If you were scraping them from the PDF, read them from the record instead.
Subsanación is the one operation that also touches the printed document: it
deliberately re-freezes the invoice's immutable recipient and issuer snapshots
from current master data, so that the PDF matches what was re-declared to the
AEAT (BR-INV-024, BR-VFC-020). It is the
only path that rewrites an already-frozen snapshot.
What reaches the AEAT
Each record transmits one declaration, chained to the previous record of the same
company by its fingerprint. Three record kinds exist, and they do not share one
chain: registrations (ALTA) and annulments (ANULACION) share the invoicing
chain, while system-event records keep a separate chain of their own, because the
regulation treats operational events as separate evidence
(BR-VFC-014).
A subsanación is declared as a new alta that always carries Subsanacion=S,
plus RechazoPrevio when it follows a rejection: X for a rejected alta of an
invoice the AEAT does not hold, S for a rejected subsanación of an invoice it does
hold, and none for a subsanación of an accepted record. Neither flag enters the
fingerprint calculation, so declaring them does not disturb the chain
(BR-VFC-026, BR-VFC-046). The records of a
company reach the AEAT in ordered batches of up to 1.000, respecting the waiting time
the AEAT states (BR-VFC-056).
You can verify the whole chain yourself with
GET /v1/companies/{company}/verifactu/chain/validate,
which recomputes every fingerprint and reports anomalies. It is rate-limited to
one call per minute per company because it walks the entire ledger.
Traceability
Derived from the domain rules of the Factuarea backend:
BR-VFC-006— the earlier capped retry policy; for billing records it is superseded byBR-VFC-040.BR-VFC-040— periodic retry, at least every hour, with no cap on attempts, in chain order, with the pending alert.BR-VFC-048— a blocked record (no automatic retry) and its release, by its cause or withretry-blocked.BR-VFC-049— a record that breaks the AEAT schema is isolated asrejected(SCHEMA_INVALID) and never sent.BR-VFC-013— the fingerprint chain is immutable and verifiable; any alteration invalidates the AEAT integrity guarantee.BR-VFC-014— the three record kinds and their independent chains.BR-VFC-015— the mandatory QR block on the printed invoice.BR-VFC-016— the CSV as public identity of the record, persisted unaltered.BR-VFC-018—no_verifactumode: local chain, no transmission.BR-VFC-020— subsanación of accepted or rejected records as a new alta, and the fingerprint guard.BR-VFC-046— several altas per invoice: ordinal,RechazoPrevioand the last alta.BR-VFC-026— the AEAT flags of a subsanación alta (Subsanacion,RechazoPrevio).BR-VFC-056— remission in ordered batches with flow control.BR-INV-024— the immutable recipient snapshot and the single exception that refreshes it.
Also derived from the transmission state machine documented alongside those rules
(AeatTransmissionStatus), which is the source of truth for the transition
matrix quoted in The five states.