Factuarea API

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_verifactu mode: 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

statusMeaningTerminal?What you do
pendingThe record exists and is chained, but has not been transmitted yet.NoNothing. Transmission is queued.
submittedSent to the AEAT, awaiting the definitive answer.NoNothing. Poll.
acceptedThe AEAT registered the declaration. aeat_csv is populated.Yes — immutableNothing. To correct the invoice, issue a corrective.
rejectedThe AEAT refused it because of a data error (unknown recipient tax ID, schema, totals).Definitive answer, but repairableFix the data, then subsanar.
errorTechnical transmission failure: timeout, AEAT unreachable, signature problem.NoNothing, 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:

FieldMeaning
statusOne of the five states above.
typeALTA (the invoice was issued) or ANULACION (it was annulled).
invoice_typeThe AEAT invoice type frozen at issue time: F1, F2, F3, R1R5.
huellaThe SHA-256 fingerprint of this record, uppercase hex. It is the chain link the next record will point at.
aeat_csvThe 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_idOur transmission identifier, for support conversations.
transmitted_atISO 8601 of the last transmission that reached the AEAT. Populated in submitted and accepted; null in pending, rejected and error.
environmentThe 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_simplifiedWhether 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 stateCauseOperationWhy
errorThe declaration never reached the AEAT.POST /v1/verifactu/records/{id}/retryThe stored XML is correct. Re-send it unchanged.
rejectedThe AEAT read it and refused the data.POST /v1/verifactu/records/{id}/subsanarThe XML must be regenerated from corrected master data.
acceptedNeither.The record is immutable. Issue a corrective invoice.
rejected, but the fix touches a fingerprint fieldThe 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, error or rejected. 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-018no_verifactu mode: 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.

On this page