VeriFactu
Alta, subsanación y anulación de registros VeriFactu. El contrato completo.
Los endpoints VeriFactu remiten registros de facturación a la AEAT. Base URL: https://sandbox.veribai.com (TEST) o https://api.veribai.com (LIVE), con autenticación x-api-key.
| Método | Endpoint | Descripción |
|---|---|---|
POST |
/v1/verifactu/crear |
Alta de un registro de facturación. |
PUT |
/v1/verifactu/subsanar |
Subsanación de un registro remitido. |
POST |
/v1/verifactu/anular |
Anulación de un registro remitido. |
El alta es síncrona y la remisión asíncrona: la petición valida, firma y encola el registro, y devuelve el QR en el momento. El envío a la AEAT se completa en segundo plano. Síguelo con GET /v1/facturas/{idFactura}/estado o recíbelo por webhook.
En LIVE, todas las operaciones exigen que el emisor tenga su representación firmada; en caso contrario devuelven
403 REPRESENTATION_PENDING. En TEST no se exige.
POST /v1/verifactu/crear
curl -X POST https://sandbox.veribai.com/v1/verifactu/crear \
-H "x-api-key: TU_CLAVE" \
-H "Content-Type: application/json" \
-d '{
"version": "1.0",
"cabecera": {
"serie": "A",
"numero": "12345",
"fechaExpedicion": "03-04-2026",
"tipoFactura": "F1",
"descripcion": "Servicios de consultoría"
},
"emisor": {
"nif": "B76116342",
"nombre": "Prueba 1 SL"
},
"destinatario": {
"nif": "B17195595",
"nombre": "Nombre cliente, SL"
},
"detalleDesglose": [
{
"impuesto": "01",
"claveRegimen": "01",
"calificacionOperacion": "S1",
"baseImponible": "100.00",
"tipoImpositivo": "21.00",
"cuotaRepercutida": "21.00"
}
],
"totales": {
"cuotaTotal": "21.00",
"importeTotal": "121.00"
}
}'
from datetime import date
from decimal import Decimal
import veribai
client = veribai.Client(api_key="TU_CLAVE", environment="test")
respuesta = client.verifactu.crear({
"version": "1.0",
"cabecera": {
"serie": "A",
"numero": "12345",
"fechaExpedicion": date(2026, 4, 3),
"tipoFactura": "F1",
"descripcion": "Servicios de consultoría",
},
"emisor": {
"nif": "B76116342",
"nombre": "Prueba 1 SL",
},
"destinatario": {
"nif": "B17195595",
"nombre": "Nombre cliente, SL",
},
"detalleDesglose": [
{
"impuesto": "01",
"claveRegimen": "01",
"calificacionOperacion": "S1",
"baseImponible": Decimal("100.00"),
"tipoImpositivo": Decimal("21.00"),
"cuotaRepercutida": Decimal("21.00"),
},
],
"totales": {
"cuotaTotal": Decimal("21.00"),
"importeTotal": Decimal("121.00"),
},
})
print(respuesta["idFactura"], respuesta["estado"])
const respuesta = await fetch("https://sandbox.veribai.com/v1/verifactu/crear", {
method: "POST",
headers: {
"x-api-key": "TU_CLAVE",
"Content-Type": "application/json",
},
body: JSON.stringify({
"version": "1.0",
"cabecera": {
"serie": "A",
"numero": "12345",
"fechaExpedicion": "03-04-2026",
"tipoFactura": "F1",
"descripcion": "Servicios de consultoría"
},
"emisor": {
"nif": "B76116342",
"nombre": "Prueba 1 SL"
},
"destinatario": {
"nif": "B17195595",
"nombre": "Nombre cliente, SL"
},
"detalleDesglose": [
{
"impuesto": "01",
"claveRegimen": "01",
"calificacionOperacion": "S1",
"baseImponible": "100.00",
"tipoImpositivo": "21.00",
"cuotaRepercutida": "21.00"
}
],
"totales": {
"cuotaTotal": "21.00",
"importeTotal": "121.00"
}
}),
});
if (!respuesta.ok) throw new Error(`VeriBai ${respuesta.status}`);
const datos = await respuesta.json();
Campos
| Campo | Notas |
|---|---|
version |
Obligatorio. Actualmente "1.0". |
cabecera.serie |
Opcional. Se admite factura sin serie. |
cabecera.numero |
Obligatorio. La numeración debe ser correlativa por serie. |
cabecera.fechaExpedicion |
Obligatorio. Formato dd-mm-aaaa; no puede ser posterior a hoy, en hora peninsular. Ver Fechas de la factura. |
cabecera.fechaOperacion |
Opcional, dd-mm-aaaa, si difiere de la expedición. Cada administración aplica sus propias reglas. Ver Fechas de la factura. |
cabecera.tipoFactura |
Obligatorio: F1–F3, R1–R5. Ver tipos de factura. |
cabecera.descripcion |
Obligatorio. Descripción de la operación, máx. 500 caracteres. |
emisor |
Obligatorio: nif y nombre. El NIF debe corresponder a un cliente dado de alta. |
destinatario |
Obligatorio salvo en F2 y R5 (simplificadas), que no lo admiten. nombre más nif o idOtro (identificación extranjera: codigoPais, idType, id). |
detalleDesglose |
Obligatorio. De 1 a 12 líneas de desglose fiscal: baseImponible obligatoria; impuesto, claveRegimen, calificacionOperacion, operacionExenta, tipoImpositivo, cuotaRepercutida y los campos de recargo de equivalencia según la operación. Importes como cadenas decimales. |
totales |
Obligatorio: cuotaTotal e importeTotal. Deben cuadrar con el desglose. |
rectificativa |
Solo en tipos R1–R5: tipo (S sustitución, I diferencias), facturasRectificadas y, si tipo=S, importe con los importes rectificados. |
especial |
Opcional. Supuestos especiales: emisión por tercero o por destinatario (emitidaPor, tercero), simplificadas cualificadas. |
aux.refExterna |
Opcional. Tu referencia interna; viaja con el registro. |
subsanacion |
Opcional, booleano (false por defecto). En la práctica no lo envías: PUT /subsanar lo marca automáticamente. |
idMaquina |
Opcional. Identificador del dispositivo o TPV emisor (máx. 64, A-Za-z0-9._-). Es metadato salvo en los emisores con Alta capacidad activa, donde encamina la cadena de encadenado. |
Tipos y formato del cuerpo
La validación ocurre antes de construir el XML, para que un dato mal formado sea un 400 inmediato y no un rechazo de la AEAT con el registro ya firmado:
cabecera.serieycabecera.numeroadmiten solo ASCII imprimible: letras sin acentos, dígitos y puntuación ASCII.A/2026,A 26,A#1yA-B_C.Dson válidos;Ñ, cualquier acento o€devuelven400nombrando el campo, con la regla AEAT1130, la misma que responde la AEAT ante unNumSerieFacturano ASCII.totales.importeTotalno puede ser negativo enF1,F2niF3: los importes negativos corresponden a las rectificativasR1–R5.-0.00no cuenta como negativo.- En las referencias de
rectificativa.facturasRectificadasyfacturasSustituidas,serieynumerosuman 60 caracteres como máximo (elTextoIDFacturaTypede la especificación, el mismo límite que el número de la propia factura). - Los campos de texto no pueden contener caracteres de control ilegales en XML, ni entidades XML o HTML ya codificadas (
<,�,&…). El escapado del XML lo hace la API, así que ese texto es una doble codificación y devuelve400nombrando el campo. Un&suelto dentro de un nombre (Tom & Jerry) es correcto. - En simplificadas
F2rige el tope de 3.000 € de la regla AEAT1150. Lo que se compara es la suma debaseImponibleycuotaRepercutidade todas las líneas dedetalleDesglose, nototales.importeTotal: el total arrastra además el recargo de equivalencia, que la regla no cuenta, así que unaF2con recargo puede pasar de 3.000 € de total y seguir siendo válida. El tope es exclusivo: 3.000,00 € se acepta y 3.000,01 € devuelve400nombrandodetalleDesglosecon la regla1150. LasR5no tienen tope, porque la regla nombra solo aF2. La API tampoco lo aplica a laF2que declaraespecial.facturaSinIdentifDestinatarioArt61d: "S", por ser una factura sin identificación del destinatario (art. 6.1.d RD 1619/2012) y no una simplificada. Atención al tipo: ese campo es la cadena"S", no un booleano.truedevuelve un400por tipo de campo, no por el tope, y"N"deja la factura sujeta a él. - VeriFactu no tiene
lineas: el desglose fiscal va endetalleDesglosey no existe un bloque de líneas de detalle en el esquema de la AEAT. Una clavelineasse ignora. Las líneas de detalle son cosa de TicketBAI.
Respuesta
200 OK. El registro queda validado, firmado y encolado:
{
"nifEmisor": "B76116342",
"idFactura": "…",
"estado": "pendiente_proceso",
"serieNumero": "A12345",
"tipoFactura": "F1",
"importeTotal": "121.00",
"cuotaTotal": "21.00",
"creadoEn": "2026-04-03T10:00:00Z",
"urlValidacion": "https://…",
"qrBase64": "…"
}
Guarda idFactura: es la clave para consultar estado, QR y XML. avisos, cuando aparece, lista avisos de validación no bloqueantes.
Los duplicados son idempotentes: reenviar un alta con la misma identidad (emisor, serie, número, fecha) devuelve 200 con el registro ya existente, no un error. En la repetición, estado trae el valor de ciclo de vida (registrada, anulada o rectificada) y el QR se regenera.
PUT /v1/verifactu/subsanar
Corrige un registro ya remitido cuando la operación es correcta pero el registro contiene datos mal informados. El cuerpo es el mismo que el de crear, con los datos corregidos. La API marca la subsanación automáticamente.
Un campo adicional: rechazoPrevio ("N" por defecto, "S" si la AEAT rechazó el registro original, "X" si el registro no consta en la AEAT). Es el único de estos indicadores que no es booleano: son tres estados, y "X" no se puede expresar con true/false.
rechazoPrevio pertenece a la subsanación. La AEAT no admite "S" ni "X" en un alta normal. Enviarlos en POST /crear sin marcar subsanacion responde 400 VALIDATION_ERROR nombrando la combinación. En PUT /subsanar no hay nada que vigilar: la API marca la subsanación por ti.
Cuándo subsanar y cuándo emitir rectificativa: Rectificar y anular.
POST /v1/verifactu/anular
Deja sin efecto un registro remitido. El cuerpo identifica la factura original:
curl -X POST https://sandbox.veribai.com/v1/verifactu/anular \
-H "x-api-key: TU_CLAVE" \
-H "Content-Type: application/json" \
-d '{
"version": "1.0",
"facturaAnulada": {
"nifEmisor": "B76116342",
"serie": "A",
"numero": "12345",
"fechaExpedicion": "03-04-2026"
}
}'
from datetime import date
import veribai
client = veribai.Client(api_key="TU_CLAVE", environment="test")
respuesta = client.verifactu.anular({
"version": "1.0",
"facturaAnulada": {
"nifEmisor": "B76116342",
"serie": "A",
"numero": "12345",
"fechaExpedicion": date(2026, 4, 3),
},
})
const respuesta = await fetch("https://sandbox.veribai.com/v1/verifactu/anular", {
method: "POST",
headers: {
"x-api-key": "TU_CLAVE",
"Content-Type": "application/json",
},
body: JSON.stringify({
"version": "1.0",
"facturaAnulada": {
"nifEmisor": "B76116342",
"serie": "A",
"numero": "12345",
"fechaExpedicion": "03-04-2026"
}
}),
});
if (!respuesta.ok) throw new Error(`VeriBai ${respuesta.status}`);
const datos = await respuesta.json();
| Campo | Notas |
|---|---|
version |
Obligatorio. Actualmente "1.0". |
facturaAnulada |
Obligatorio: nifEmisor, numero y fechaExpedicion; serie si la factura la tenía. serie + numero + fechaExpedicion deben coincidir exactamente con la factura original (la fecha forma parte de su identidad), y la fecha no puede ser futura: identifica una factura ya expedida. |
refExterna |
Opcional. Referencia interna. |
sinRegistroPrevio |
Opcional, booleano (false por defecto). true declara que el alta nunca llegó a registrarse. |
rechazoPrevio |
Opcional, cadena "N"/"S". "S" si la AEAT rechazó la anulación anterior. La anulación no admite "X". Ese valor solo existe en el alta y en la subsanación. |
idMaquina |
Opcional. |
Respuesta
A diferencia del alta, la respuesta viene envuelta en el sobre { code, message, data }; la remisión a la AEAT es asíncrona y estado arranca en pendiente_envio:
{
"code": "SUCCESS",
"message": "Anulación registrada correctamente",
"data": {
"nifEmisor": "B76116342",
"idFactura": "…",
"estado": "pendiente_envio",
"serieNumero": "A12345",
"fechaExpedicion": "03-04-2026",
"creadoEn": "2026-04-03T11:00:00Z",
"mensaje": "La anulación ha sido registrada y será procesada para envío a AEAT. …"
}
}
La anulación genera su propio registro encadenado: nada desaparece de la cadena.
Tipos de factura
| Tipo | Descripción |
|---|---|
F1 |
Factura completa, con destinatario identificado. |
F2 |
Factura simplificada, sin destinatario. Tope de 3.000 € sobre el desglose; ver tipos y formato del cuerpo. |
F3 |
Factura emitida en sustitución de simplificadas. |
R1 |
Rectificativa: error fundado en derecho. |
R2 |
Rectificativa: art. 80.3 LIVA (concurso de acreedores). |
R3 |
Rectificativa: art. 80.4 LIVA (créditos incobrables). |
R4 |
Rectificativa: resto de casos. |
R5 |
Rectificativa de factura simplificada. |
Errores
Los códigos comunes a toda la API están en Errores. Específicos de estos endpoints:
| Código | Cuándo |
|---|---|
400 VALIDATION_ERROR |
El cuerpo no supera la validación; errors enumera cada campo. |
400 R3_TIMING_ERROR |
crear: rectificativa R3 con menos de seis meses desde la factura que rectifica. Ver Rectificar y anular. |
400 DATE_FORMAT_ERROR |
crear, solo en R3: una de las fechas de la comparación no pudo interpretarse (DD-MM-YYYY). |
400 MISSING_BODY |
anular: la petición no lleva cuerpo. |
403 UNAUTHORIZED_NIF |
El emisor.nif no está dado de alta bajo tu cuenta. |
403 REPRESENTATION_PENDING |
Solo LIVE: el emisor no tiene la representación firmada. |
409 DUPLICATE_INVOICE |
Caso límite: el registro existe pero sus datos no pudieron recuperarse. El duplicado normal no es un error, sino una repetición idempotente con 200. |
500 AUTH_ERROR |
anular: falló el paso de autorización. No es una denegación. Reintentable. |