← Blog

VeriFactu against the Spanish tax agency: the three errors that stopped us, and the fixes

We implemented VeriFactu from scratch for our ERP and submitted it to the AEAT pre-production environment. We hit errors 4104, 1189 and 1110 back to back. Here's what each one means and what had to change.

Why we're writing this

VeriFactu is Spain's new invoicing regime, and it fits in one sentence: every invoice you issue leaves a SHA-256 hash chained to the previous one, and the system issuing it has to identify itself to the tax agency. Sounds simple. The official documentation is several PDFs plus a validation catalogue, and the usual path is discovering the rules one at a time, by getting your submissions rejected.

We implemented it from scratch for our ERP, in Dart, and submitted it to the AEAT pre-production environment. This article is the list of what blocked us, with the error number up front, because that's what we wish someone had written down.

Honesty note: pre-production is not production. It's the tax agency's test environment, with no fiscal effect. Everything below was verified there.

The setup, in four pieces

Before the errors, here's what you need in place:

  1. The record (RegistroAlta): the invoice as XML, with its VAT breakdown.
  2. The hash: SHA-256 as 64 hex characters in upper case, computed over specific fields in a specific order, and chained to the previous invoice's hash. This is what makes it impossible to delete an invoice from the middle without it showing.
  3. The submission: SOAP over mTLS. The certificate isn't just for encryption, it's your identity. In VeriFactu mode there is no XAdES signature; authentication comes from the submission certificate and integrity from the hash chain.
  4. The verification QR code: the little square printed on the receipt that lets anyone check the invoice against the tax agency.

The hash is the scary-looking part and gave us the least trouble: you can validate it locally against known test vectors before talking to anyone. Do that. Arriving with an already-validated hash removes one variable once the rejections start.

Error 4104 — "ObligadoEmisión tax ID not identified"

The first one, and the most baffling, because the tax ID was correct.

The rule is that the issuer's tax ID must be identified in the AEAT census, and identification goes together with the name: sending the right number isn't enough, it has to match the registered legal name for that ID.

The fix: use exactly the name that appears in the certificate as NombreRazonEmisor. The certificate already carries the tax-ID-and-name pair as the agency knows it; any variation of your own — an abbreviated company suffix, one accent too many — and it won't match.

Error 1189 — "Destinatarios missing"

This one is about cross-field rules, and it's the classic afternoon-burner.

The Destinatarios (recipients) block is not optional at your discretion: it depends on the invoice type.

  • F1, F3 and rectifying invoices R1–R4 require at least one recipient.
  • F2 (simplified) and R5 must not carry it. Send it and you get rejected.

So the same field is mandatory or forbidden depending on a letter three levels up in the XML.

The fix: for the test, issue as F2, the simplified invoice — the everyday till receipt, which carries no recipient. For named invoices you do have to send the block, and that's where the next step appears: depending on the tax regime code, your customer's tax ID also has to be identified in the census. In other words, the 4104 problem again, but now with a tax ID you don't control.

Error 1110 — "SistemaInformatico tax ID not identified"

The easiest one to misread, which is why it's worth the whole article.

The record contains a SistemaInformatico block that is not about your company or your customer's: it's about the software. Who makes it. If you sell invoicing software, that block carries your tax ID as the producer, not the shop's that is issuing the invoice.

And it has a handful of mandatory fields that are easy to miss:

  • IdSistemaInformatico: exactly 2 characters, [A-Z0-9].
  • NombreSistemaInformatico.
  • TipoUsoPosibleSoloVerifactu and TipoUsoPosibleMultiOT: both mandatory.
  • The producer's tax ID, identified in the census — with its name, same as in 4104.

The fix: the same as 4104. The name, exactly as it appears in the certificate.

The rules worth reading up front

After the third rejection we stopped guessing and read the official AEAT validation catalogue end to end. Best decision of the whole process: from that point on, the invoice passed every applicable validation first try. The ones easiest to break without noticing:

  • Issue date: cannot be in the future, nor earlier than 28/10/2024.
  • Invoice series/number: ASCII 32–126, and the characters " ' < > = are forbidden.
  • F2 simplified: base plus tax cannot exceed €3,000.
  • VAT rate: only the catalogue values are accepted (0, 2, 4, 5, 7.5, 10 and 21).
  • Arithmetic: the charged amount must be base × rate, the total tax the sum of the charged amounts, and the invoice total the sum of everything. There's a cents-level tolerance, but it is checked.
  • Record generation timestamp: drifting into the future relative to the agency's clock is a warning, not a rejection. Knowing which ones are warnings and which kill the submission is worth its own afternoon.

What we took away

Three conclusions, in case they help:

  1. Almost every early rejection is about identity, not content. The XML was fine; what failed was who each party claimed to be. Start there.
  2. Validate the hash locally before submitting anything. It's the only part you can confirm without depending on the tax agency.
  3. Read the validation catalogue before round four, not after. Fixing error by error is slow and gives you a false sense of progress.

We did all of this to bring it into Atellum, our management software for small shops and workshops: the point is that whoever uses it never has to know any of the above — which is exactly why we ate the three errors ourselves.

If you're in the same fight and get stuck on a specific code, drop us a line and we'll tell you what it was in our case.

Want to talk about your case?

Tell us what you need and we will get back to you within 24 hours with a clear proposal.

Get a quote