VeriFactu submission states
The lifecycle of a VeriFactu billing record — pending, submitted, accepted, rejected, error — what csv and huella mean, how the retry budget works, and when to retry instead of 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. To correct the invoice, issue a corrective. |
rejected | The AEAT refused it because of a data error (unknown recipient tax ID, schema, totals). | Definitive answer, but repairable | Fix the data, then subsanar. |
error | Technical transmission failure: timeout, AEAT unreachable, signature problem. | No | Nothing, or force a retry. |
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. Every other state can transition again, which is what makes retry
and subsanación 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/verifactu/records/{id},
GET /v1/verifactu/records
and, keyed by invoice, by
GET /v1/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. |
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/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"
}
}The retry budget is real, and it is not in the payload
Behind error there is a counter and a schedule. A failed transmission is
re-queued with exponential backoff, and the number of blind technical retries is
capped per transmission round; once the cap is exhausted, a further manual retry
answers with a business-rule error instead of re-queueing
(BR-VFC-006). Alongside the counter the record carries a
technical-incident marker, raised when the AEAT itself was unreachable and the
incident had to be declared — it is preserved even after a later acceptance, for
audit.
None of those three values — the attempt counter, the next scheduled retry and
the incident marker — are exposed in the v1 record object. They govern the
behaviour you observe, but you cannot read them over the public API today. What
you can observe is the state itself, transmitted_at, and the record's audit
timeline via
GET /v1/verifactu/records/{id}/activities.
Do not build a client-side model of the retry schedule from guesses: poll the
state.
Retry or subsanar
Both operations act on a record that already exists. They are not interchangeable.
| Record state | Cause | Operation | Why |
|---|---|---|---|
error | The declaration never reached the AEAT. | POST /v1/verifactu/records/{id}/retry | The stored XML is correct. Re-send it unchanged. |
rejected | The AEAT read it and refused the data. | POST /v1/verifactu/records/{id}/subsanar | The XML must be regenerated from corrected master data. |
accepted | — | Neither. | The record is immutable. Issue a corrective invoice. |
rejected, 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 consumes the technical retry budget. Subsanación does not: it is a
deliberate manual correction with new data, the regulation sets no cap on it, and
executing it resets the transmission round — the attempt counter goes back to
zero and the automatic retransmission of the corrected content gets its full
budget again (BR-VFC-006, BR-VFC-020).
Subsanación regenerates the payload but embeds the original fingerprint, the
original chain link and the original generation timestamp, because those are
what let the AEAT match the resubmission to the record it rejected. Before
persisting 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 modified. 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).
When a resubmission follows a rejection, the regenerated declaration additionally
carries the AEAT flags stating that the previous submission was refused and the
record was therefore never registered. Neither flag enters the fingerprint
calculation, so declaring them does not disturb the chain
(BR-VFC-026).
You can verify the whole chain yourself with
GET /v1/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— retry policy: exponential backoff, capped attempts per round, and the cap explicitly not applying to subsanación.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 rejected records, fingerprint guard, and the transmission-round reset.BR-VFC-026— the AEAT flags for a resubmission after rejection.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.