Integrar VeriFactu en un TPV de hostelería: ejemplos de tickets, facturas y rectificativas
· Equipo Factuneo
Las peticiones completas de una mesa de restaurante: ticket F2 con IVA al 10 % y al 21 %, factura F3 que sustituye al ticket, rectificativas R5 por diferencias y por sustitución, anulación e impresión del QR. Verificadas contra la preproducción de la AEAT.
Lo que vamos a construir
Una mesa de restaurante de principio a fin, tal como la emitiría un TPV: el ticket, la factura que lo sustituye cuando el cliente la pide y las dos formas de rectificarlo. Estas peticiones las verificamos el 22 de octubre de 2026 contra la preproducción de la AEAT: los cuatro registros quedaron en estado Correcto y la consulta posterior confirmó la huella.
Si buscas el porqué antes que el cómo, empieza por VeriFactu en hostelería.
Lo básico
| Qué | Valor |
|---|---|
| Petición | POST https://factuneo.es/api/v1/invoices |
| Entornos | Misma URL: la clave decide (vsk_test_… va a la preproducción de la AEAT, vsk_live_… a producción) |
| Cabeceras | x-api-key, Content-Type: application/json y, recomendada, Idempotency-Key (un valor único por ticket) |
| Fechas | AAAA-MM-DD. La de expedición no puede ser futura |
| Importes | Números JSON, no texto. En negativo en las rectificativas por diferencias |
| IVA | desglose (obligatorio), una línea por tipo. lineas (opcional) lleva el precioUnitario sin IVA de cada concepto y debe cuadrar con el desglose tipo a tipo |
1. El ticket (F2)
Mesa 7: dos menús y dos cafés al 10 % y una botella de vino para llevar al 21 %. Sin destinatarios: el ticket no identifica al cliente.
{
"emisorNif": "B12345674",
"numeroSerie": "T1-2026-000123",
"fechaExpedicion": "2026-10-22",
"tipoFactura": "F2",
"nombreRazonEmisor": "RESTAURANTE EJEMPLO SL",
"descripcionOperacion": "Consumo en mesa 7",
"lineas": [
{ "descripcion": "Menú del día", "cantidad": 2, "precioUnitario": 15, "tipoImpositivo": 10 },
{ "descripcion": "Café", "cantidad": 2, "precioUnitario": 1.5, "tipoImpositivo": 10 },
{ "descripcion": "Botella de vino para llevar", "cantidad": 1, "precioUnitario": 12, "tipoImpositivo": 21 }
],
"desglose": [
{ "claveRegimen": "01", "calificacionOperacion": "S1", "tipoImpositivo": 10, "baseImponible": 33, "cuotaRepercutida": 3.3 },
{ "claveRegimen": "01", "calificacionOperacion": "S1", "tipoImpositivo": 21, "baseImponible": 12, "cuotaRepercutida": 2.52 }
],
"baseImponible": 45,
"cuotaIva": 5.82,
"importeTotal": 50.82
}
curl -X POST https://factuneo.es/api/v1/invoices \
-H "x-api-key: vsk_test_..." \
-H "Idempotency-Key: mesa7-20261022-1" \
-H "Content-Type: application/json" \
-d @ticket.json
La respuesta llega al momento, antes de que el registro se envíe a la AEAT:
{
"invoiceId": "3f8a1c2e-…",
"hash": "A5C114D6…",
"qrUrl": "https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR?nif=B12345674&numserie=T1-2026-000123&fecha=22-10-2026&importe=50.82",
"qrString": "https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR?nif=B12345674&numserie=T1-2026-000123&fecha=22-10-2026&importe=50.82"
}
qrString es el texto que se codifica en el QR del ticket. Guarda invoiceId: con él se consulta, se anula o se descarga el PDF.
Un límite a tener en cuenta: en hostelería la factura simplificada llega hasta 3.000 €, IVA incluido. Por encima, la AEAT rechaza el F2 (error 1150): hay que emitir una factura completa.
2. El cliente pide factura (F3)
Cuando el cliente pide factura de un ticket ya emitido, no se emite otra venta: se emite una F3 que sustituye al ticket, con sus datos y su domicilio en destinatarios y el ticket en facturasSustituidas.
{
"emisorNif": "B12345674",
"numeroSerie": "F-2026-000045",
"fechaExpedicion": "2026-10-22",
"tipoFactura": "F3",
"nombreRazonEmisor": "RESTAURANTE EJEMPLO SL",
"descripcionOperacion": "Consumo en mesa 7 (sustituye al ticket T1-2026-000123)",
"facturasSustituidas": [{ "numeroSerie": "T1-2026-000123", "fechaExpedicion": "2026-10-22" }],
"destinatarios": [{
"nombreRazon": "CLIENTE EMPRESA SA",
"nif": "A28000024",
"domicilio": { "direccion": "Rúa do Vilar 10", "codigoPostal": "15705", "municipio": "Santiago de Compostela", "provincia": "A Coruña" }
}],
"lineas": [
{ "descripcion": "Menú del día", "cantidad": 2, "precioUnitario": 15, "tipoImpositivo": 10 },
{ "descripcion": "Café", "cantidad": 2, "precioUnitario": 1.5, "tipoImpositivo": 10 },
{ "descripcion": "Botella de vino para llevar", "cantidad": 1, "precioUnitario": 12, "tipoImpositivo": 21 }
],
"desglose": [
{ "claveRegimen": "01", "calificacionOperacion": "S1", "tipoImpositivo": 10, "baseImponible": 33, "cuotaRepercutida": 3.3 },
{ "claveRegimen": "01", "calificacionOperacion": "S1", "tipoImpositivo": 21, "baseImponible": 12, "cuotaRepercutida": 2.52 }
],
"baseImponible": 45,
"cuotaIva": 5.82,
"importeTotal": 50.82
}
El PDF de la factura, con los conceptos, los domicilios y el QR: GET /api/v1/invoices/{invoiceId}/pdf con la misma clave.
3. Devolver un café (R5 por diferencias)
Solo la variación, en negativo, y el ticket que se rectifica:
{
"emisorNif": "B12345674",
"numeroSerie": "R-2026-000007",
"fechaExpedicion": "2026-10-22",
"tipoFactura": "R5",
"rectificativa": {
"tipo": "I",
"facturasRectificadas": [{ "numeroSerie": "T1-2026-000123", "fechaExpedicion": "2026-10-22" }],
"motivo": "Devolución de un café no servido"
},
"nombreRazonEmisor": "RESTAURANTE EJEMPLO SL",
"descripcionOperacion": "Devolución parcial del ticket T1-2026-000123",
"lineas": [{ "descripcion": "Café", "cantidad": -1, "precioUnitario": 1.5, "tipoImpositivo": 10 }],
"desglose": [
{ "claveRegimen": "01", "calificacionOperacion": "S1", "tipoImpositivo": 10, "baseImponible": -1.5, "cuotaRepercutida": -0.15 }
],
"baseImponible": -1.5,
"cuotaIva": -0.15,
"importeTotal": -1.65
}
4. Rehacer la cuenta (R5 por sustitución)
Los importes correctos completos en el desglose y, en importeRectificacion, los del ticket original. Aquí se cobró un menú que no se sirvió:
{
"emisorNif": "B12345674",
"numeroSerie": "R-2026-000008",
"fechaExpedicion": "2026-10-22",
"tipoFactura": "R5",
"rectificativa": {
"tipo": "S",
"facturasRectificadas": [{ "numeroSerie": "T1-2026-000123", "fechaExpedicion": "2026-10-22" }],
"importeRectificacion": { "baseRectificada": 45, "cuotaRectificada": 5.82 },
"motivo": "Se cobró un menú que no se sirvió"
},
"nombreRazonEmisor": "RESTAURANTE EJEMPLO SL",
"descripcionOperacion": "Sustituye al ticket T1-2026-000123",
"lineas": [
{ "descripcion": "Menú del día", "cantidad": 1, "precioUnitario": 15, "tipoImpositivo": 10 },
{ "descripcion": "Café", "cantidad": 2, "precioUnitario": 1.5, "tipoImpositivo": 10 },
{ "descripcion": "Botella de vino para llevar", "cantidad": 1, "precioUnitario": 12, "tipoImpositivo": 21 }
],
"desglose": [
{ "claveRegimen": "01", "calificacionOperacion": "S1", "tipoImpositivo": 10, "baseImponible": 18, "cuotaRepercutida": 1.8 },
{ "claveRegimen": "01", "calificacionOperacion": "S1", "tipoImpositivo": 21, "baseImponible": 12, "cuotaRepercutida": 2.52 }
],
"baseImponible": 30,
"cuotaIva": 4.32,
"importeTotal": 34.32
}
5. Un ticket que no debió existir: anular
Si el ticket se emitió por error —la mesa equivocada, una venta que no hubo—, no se rectifica: se anula su registro.
curl -X POST https://factuneo.es/api/v1/invoices/3f8a1c2e-…/anular \
-H "x-api-key: vsk_test_..." -H "Content-Type: application/json" -d '{}'
Imprimir el QR en el ticket
Según la especificación del código QR de la AEAT:
- entre 30×30 y 40×40 mm, norma ISO/IEC 18004:2015 y nivel M de corrección de errores;
- al principio del ticket, con al menos 2 mm de margen en blanco alrededor (se recomiendan 6 mm);
- encima, «QR tributario:»; justo debajo, «Factura verificable en la sede electrónica de la AEAT» o «VERI*FACTU».
En una impresora térmica de 80 mm cabe sin problema: codifica qrString tal cual, sin acortarlo.
Saber si la AEAT lo aceptó
El envío a la AEAT va por detrás, con reintentos si la AEAT no responde. El resultado te llega por webhook (invoice.accepted, invoice.rejected o invoice.error) o con GET /api/v1/invoices/{invoiceId}. Para el TPV, lo normal es imprimir con la respuesta inmediata y reflejar el estado en el backoffice.
Conexión y declaración responsable
- Cada ticket necesita conexión: la huella y el QR se generan en la API al recibir la petición, y la AEAT exige que el ticket se entregue ya con su QR. Las caídas de la AEAT, en cambio, no te afectan.
- El TPV que imprime el QR es un componente del sistema y, según las aclaraciones de la AEAT a desarrolladores, necesita su propia declaración responsable.
Siguiente paso
Crea una cuenta (sin tarjeta), copia tu clave vsk_test_ y lanza el ticket de la mesa 7 contra la preproducción de la AEAT. Tienes todas las recetas y la referencia de campos en la documentación.