Spanish invoicing software has to print a tax QR code on invoices under the Verifactu regime. This guide covers what that QR encodes, where the rules come from, how to generate it on the server, and how to place it in an HTML template that you render to PDF.
Everything here is taken from the consolidated text of Real Decreto 1007/2023, Orden HAC/1177/2024, and the AEAT technical document version 0.5.0 dated 10 December 2025. Where a fact is not in those sources, this article says so.
What is the Verifactu QR code?
The Verifactu QR code is a graphical element that invoicing software must print on the invoice, encoding a single HTTPS URL that points at an AEAT service where the recipient can check the invoice against the tax agency's records. It is defined in article 20.1.a) of Orden HAC/1177/2024, which develops article 6.5 of the regulation approved by Real Decreto 1007/2023.
The order is explicit that the requirement covers both media: "tanto si está impresa en soporte papel como si se trata de la imagen de la misma en soporte digital". A PDF invoice counts. The same article adds that the elements must be legible and printed at an appropriate resolution, which is a rendering requirement, not just a data requirement.
There is one carve-out. For a structured electronic invoice exchanged machine to machine, article 20.2 says the URL goes in its own field and the QR image itself is not required.
When does it become mandatory?
Two dates, both in 2027, after an extension published in December 2025.
Real Decreto-ley 15/2025, of 2 December, published in the BOE on 3 December 2025, amended the fourth final provision of Real Decreto 1007/2023 and pushed the adaptation deadlines back by a year.
| Who | Deadline | Source |
|---|---|---|
| Taxpayers under article 3.1.a): corporate income tax payers | 1 January 2027 | Disposición final cuarta, RD 1007/2023, as amended by RDL 15/2025 |
| All other taxpayers under article 3.1: personal income tax payers with economic activity, non-resident income tax payers with a permanent establishment, income-attribution entities | 1 July 2027 | Same |
| Producers and sellers of invoicing software: compliant products on offer | Nine months from the entry into force of Orden HAC/1177/2024, and in any case before the dates above | Same |
Article 3.1 of the regulation defines those four categories. If you sell invoicing software into Spain, the software vendor deadline is the one that binds you first, and it has already passed in calendar terms, so the product requirement is live today even though the taxpayer obligation is not.
What exactly is encoded in the QR?
A plain HTTPS URL with four mandatory query parameters. No encryption, no signature, no compression.
The base URL depends on whether the issuing system sends every invoicing record to the AEAT ("sistema de emisión de facturas verificables", the actual VERI*FACTU mode) or keeps records locally. Production endpoints, quoted from the AEAT technical document:
Verifactu mode (records sent to AEAT):
https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR
Non-Verifactu mode (records kept locally):
https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQRNoVerifactu
External testing portal:
https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR
https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQRNoVerifactuThe four parameters, with the formats given in section 6 of the technical document:
| Parameter | Format | Length | Description |
|---|---|---|---|
nif | NIF format | 9 | Tax number of the party obliged to issue the invoice |
numserie | Text, may contain special characters, printable ASCII 32 to 126 only | 60 max | Series number plus invoice number |
fecha | Date with hyphens, DD-MM-AAAA | 10 | Invoice issue date |
importe | Numeric, dot as decimal separator | 12 integer digits, 2 decimals max | Invoice total |
A production example from the document itself:
https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR?nif=89890001K&numserie=12345678-G33&fecha=01-09-2024&importe=241.4Two optional parameters exist on the service, idioma (values gl, ca, eu, es, va, en) and formato=json. The document is emphatic that formato must never be placed in the URL encoded into the invoice QR. It exists so a recipient's software can call the same URL from an electronic invoice field and parse a JSON answer.
How to generate the URL without breaking it
The invoice number is the parameter that breaks implementations. It is free text, it commonly contains characters like &, +, # or a space, and those characters change the meaning of a query string.
Section 4 of the technical document requires URL encoding of the parameter values using UTF-8, and gives a worked example: a numserie of 12345678&G33 must be written as 12345678%26G33. The same section restricts text values to printable ASCII, codes 32 to 126, so an accented character in a series prefix is out of spec.
In Node.js, build the URL with the standard library rather than string concatenation:
type Invoice = {
issuerNif: string;
seriesAndNumber: string;
issueDate: Date;
totalAmount: number;
};
const AEAT_QR_BASE = {
verifactu: "https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR",
noVerifactu:
"https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQRNoVerifactu",
} as const;
function spanishDate(d: Date): string {
// DD-MM-AAAA, as required by the AEAT parameter table
const p = (n: number) => String(n).padStart(2, "0");
return `${p(d.getDate())}-${p(d.getMonth() + 1)}-${d.getFullYear()}`;
}
export function buildVerifactuQrUrl(
invoice: Invoice,
mode: keyof typeof AEAT_QR_BASE,
): string {
const url = new URL(AEAT_QR_BASE[mode]);
// URLSearchParams applies UTF-8 percent-encoding to every value
url.searchParams.set("nif", invoice.issuerNif);
url.searchParams.set("numserie", invoice.seriesAndNumber);
url.searchParams.set("fecha", spanishDate(invoice.issueDate));
url.searchParams.set("importe", invoice.totalAmount.toFixed(2));
return url.toString();
}One caveat on URLSearchParams: it encodes a space as +, which is the form-encoding convention rather than the percent-encoding one. If your series numbers contain spaces, replace them or normalise the output with encodeURIComponent per value and assemble the query yourself.
Generating the QR image server side
The QR must follow ISO/IEC 18004:2015 with error correction level M, per article 21.1. Most libraries default to level M already, but set it explicitly so a library upgrade cannot silently change it.
Produce the image on the server and put it into the HTML as a data URI. That removes the network round trip and the timing risk described later.
import QRCode from "qrcode";
export async function verifactuQrDataUri(url: string): Promise<string> {
return QRCode.toDataURL(url, {
errorCorrectionLevel: "M", // required by article 21.1
type: "image/png",
margin: 0, // quiet zone handled in CSS, see below
scale: 12, // ~ 500 px for a typical version, plenty for 40 mm at 300 dpi
});
}Python, with segno:
import io, base64
import segno
def verifactu_qr_data_uri(url: str) -> str:
qr = segno.make(url, error="m")
buf = io.BytesIO()
qr.save(buf, kind="png", scale=12, border=0)
encoded = base64.b64encode(buf.getvalue()).decode("ascii")
return "data:image/png;base64," + encodedAn SVG output works too and avoids the resolution question entirely. If you keep PNG, size it for print: a 40 mm square at 300 dpi is about 472 pixels, so anything under roughly 500 pixels of source image risks a soft, unreliable symbol once printed.
Placing it correctly in an HTML to PDF template
The placement rules in section 3 of the AEAT document are specific, and they map cleanly onto CSS.
The QR goes at the start of the invoice, before the content generated by the invoicing system, and appears once, on the first page, for a multi-page invoice. On a portrait layout it sits near the top margin, preferably centred horizontally. On a landscape layout it sits on the left, near the top-left margin. The literal text QR tributario: must always precede it, placed above the code. For invoices issued by a Verifactu system, the phrase Factura verificable en la sede electrónica de la AEAT or VERI*FACTU goes directly below, preferably centred. Both texts must use a font size equal to or larger than the rest of the invoice data.
The quiet zone is at least 2 mm of empty white space on all four sides, 6 mm recommended, and the contrast between the code and the background must be high enough to stay readable.
<div class="tax-qr">
<p class="tax-qr__label">QR tributario:</p>
<img class="tax-qr__img" src="data:image/png;base64,iVBORw0KG..." alt="QR tributario" />
<p class="tax-qr__phrase">VERI*FACTU</p>
</div>@page {
size: A4;
margin: 15mm;
}
.tax-qr {
text-align: center;
/* 6 mm recommended quiet zone, applied as padding on a white box */
padding: 6mm;
background: #ffffff;
break-inside: avoid;
break-after: avoid;
}
.tax-qr__img {
width: 35mm; /* inside the 30 mm to 40 mm range */
height: 35mm;
display: block;
margin: 0 auto;
image-rendering: pixelated; /* keeps module edges crisp when scaled */
}
.tax-qr__label,
.tax-qr__phrase {
font-size: 11pt; /* equal to or larger than the invoice body text */
margin: 2mm 0;
}Two details worth calling out. width and height in millimetres give a physical size in a paged-media renderer, which is what the 30 to 40 mm rule asks for, so avoid pixel sizes here. And break-inside: avoid keeps the label, code and phrase together if the header ever grows.
If your renderer repeats a header on every page, the QR block must not live inside it. The rule is one QR on page one only.
Four failure modes to watch for
A client-side QR that never appears. If the QR is drawn into a canvas by a script at load time, a headless renderer may take its snapshot before the script finishes, and the PDF ships with an empty box. Generating the image on the server and inlining it as a data URI removes the race entirely. The general QR in PDF guide covers this pattern in more depth for non-Spanish use cases.
Insufficient print resolution. Article 20.1 asks for elements that are legible and printed at an appropriate resolution. A 200 pixel PNG scaled up to 40 mm on paper produces blurry modules, and level M error correction only recovers about 15 percent of the symbol. Either use SVG, or generate the PNG at 500 pixels or more.
Characters outside printable ASCII. A series prefix with an accent or an em-space is outside the ASCII 32 to 126 range the specification allows, and the checking service will reject the URL on format validation.
Amount and date formats copied from your locale. importe uses a dot as decimal separator regardless of Spanish display conventions, and fecha is DD-MM-AAAA with hyphens. A comma separator or an ISO date fails validation.
Verifactu is not the B2B electronic invoicing mandate
These are two different Spanish rules and confusing them wastes planning time.
| Verifactu | B2B electronic invoicing | |
|---|---|---|
| Legal base | Real Decreto 1007/2023, Orden HAC/1177/2024 | Ley 18/2022 (Crea y Crece), Real Decreto 238/2026 |
| What it governs | How invoicing software records invoices, the record chain, the tax QR, optional real-time transmission to AEAT | Structured invoice exchange between businesses, formats, platforms, payment status reporting |
| Artefact on the invoice | QR code plus, in Verifactu mode, the "Factura verificable" phrase | A structured invoice file exchanged between systems |
| Calendar | 1 January 2027 and 1 July 2027 | Phased, tied to implementing rules |
Real Decreto 238/2026, of 25 March, published in the BOE on 31 March 2026, is the implementing regulation for the B2B mandate. Its fourth final provision sets two phases, 12 months for businesses with turnover above 8 million euros and 24 months for everyone else. What those periods count from depends on further implementing rules, so a firm calendar date for the B2B mandate is not something this article can state from a primary source today. Treat any specific B2B date you read elsewhere as provisional until it appears in the BOE.
The practical consequence for a PDF pipeline: the QR work described above is required for the invoice document itself, and it is independent of whatever structured format your B2B exchange ends up using. Build it once, in the template, and it keeps working.
Checklist before you ship
- QR encodes the production endpoint matching your mode,
ValidarQRorValidarQRNoVerifactu - All four parameters present, values URL-encoded in UTF-8, text values within printable ASCII
fechaasDD-MM-AAAA,importewith a dot separatorformato=jsonnever in the invoice QR- ISO/IEC 18004:2015, error correction level M
- Printed size between 30 and 40 mm square, quiet zone of 2 mm minimum, 6 mm recommended
QR tributario:above the code,VERI*FACTUor the full phrase below it when in Verifactu mode- QR on page one only, at the top of the page, not inside a repeating header
- Image generated server side and inlined, not fetched or drawn by a browser script
Test against the external testing portal endpoints before production. The AEAT document includes the error code list returned for missing parameters, malformed fields and bad NIF format, which is the fastest way to validate your URL builder against real behaviour rather than against your own reading of the spec.
Free tools mentioned:
Start generating PDFs
Build PDF templates with a visual editor. Render them via API from any language in ~300ms.



