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 (Sandbox) 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 Sandbox 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": "B00000000",
"nombre": "Prueba 1 SL"
},
"destinatario": {
"nif": "B11111119",
"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": "B00000000",
"nombre": "Prueba 1 SL",
},
"destinatario": {
"nif": "B11111119",
"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": "B00000000",
"nombre": "Prueba 1 SL"
},
"destinatario": {
"nif": "B11111119",
"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();
{
"nifEmisor": "B00000000",
"idFactura": "…",
"estado": "pendiente_proceso",
"numSerieFactura": "A12345",
"serie": "A",
"numero": "12345",
"tipoFactura": "F1",
"importeTotal": "121.00",
"cuotaTotal": "21.00",
"creadoEn": "2026-04-03T10:00:00Z",
"urlValidacion": "https://…",
"qrBase64": "…"
}
El registro queda validado, firmado y encolado. 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. La repetición lleva "yaExistente": true, estado trae el valor de ciclo de vida (registrada, anulada o rectificada) y el QR se regenera. yaExistente: true indica que VeriBai ya tenía esa factura y no ha creado nada; en una factura nueva el campo no aparece.
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, nunca los dos. Ver destinatario extranjero. |
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. Ver claves por impuesto. |
totales |
Obligatorio: cuotaTotal e importeTotal, exactamente como figuran en la factura, con el recargo de equivalencia incluido. Se validan contra el desglose con una tolerancia de ±10 € (no se comprueba con las claves 03, 05, 06, 08 o 09) y se transmiten tal como se declaran. |
rectificativa |
Solo en tipos R1–R5: tipo (S sustitución, I diferencias), facturasRectificadas y, si tipo=S, importe con los importes rectificados. |
especial |
Opcional. Indicadores especiales de la AEAT: simplificadas cualificadas, factura sin identificación del destinatario, emisión por tercero o por destinatario, cupón y acuerdos registrados. Ver supuestos especiales. |
aux.refExterna |
Opcional. Se acepta y no se transmite a la AEAT. |
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: un dato mal formado responde 400 de inmediato, sin llegar a firmarse.
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 aplica la AEAT a 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. Ver el tope de las simplificadas. - 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 existen solo en TicketBAI.
El tope de las simplificadas F2
La regla AEAT 1150 limita las simplificadas F2 a 3.000 €. La API la comprueba en el alta.
| Qué se compara | La suma de baseImponible y cuotaRepercutida de todas las líneas de detalleDesglose. No totales.importeTotal: el total incluye el recargo de equivalencia, que la regla no cuenta. Una F2 con recargo puede superar 3.000 € de total y ser válida. |
| Límite | Exclusivo. 3.000,00 € se acepta; 3.000,01 € devuelve 400 nombrando detalleDesglose con la regla 1150. |
| Exentas | Las R5: la regla nombra solo a F2. La F2 con especial.facturaSinIdentifDestinatarioArt61d: "S": es una factura sin identificación del destinatario (art. 6.1.d RD 1619/2012), no una simplificada. La F2 con especial.numRegistroAcuerdoFacturacion. |
facturaSinIdentifDestinatarioArt61d es la cadena "S", no un booleano. true devuelve 400 por tipo de campo, no por el tope. "N" deja la factura sujeta al tope.
Supuestos especiales
El bloque especial recoge los indicadores opcionales del registro de alta. Todos se transmiten a la AEAT.
| Campo | Notas |
|---|---|
facturaSimplificadaArt7273 |
"S" o "N". "S" solo en F1, F3 y R1–R4: simplificada cualificada (arts. 7.2 y 7.3 RD 1619/2012). |
facturaSinIdentifDestinatarioArt61d |
"S" o "N". "S" solo en F2 y R5. Exime a la F2 del tope de 3.000 €. |
emitidaPor |
"D" o "T". Omitido, se aplica el modelo de emisión de tu cuenta. "D": la factura la expide el destinatario; exige destinatario. "T": la expide el tercero indicado en tercero. |
tercero |
Con emitidaPor: "T": nombre más nif o idOtro. Sustituye al tercero por defecto. Un nif igual al del emisor devuelve 400 con la regla AEAT 1188. |
cupon |
"S" o "N". "S" solo en R1 y R5. |
numRegistroAcuerdoFacturacion |
Máx. 15 caracteres. Número de registro del acuerdo de facturación autorizado por la AEAT, que comprueba que existe. Exime a la F2 del tope de 3.000 €. |
idAcuerdoSistemaInformatico |
Máx. 16 caracteres. Identificador del acuerdo del sistema informático registrado en la AEAT. |
Los indicadores son las cadenas "S" y "N", no booleanos. "N" equivale a omitir el campo. Un "S" en un tipo de factura no admitido devuelve 400 nombrando el campo.
Macrodato no es un campo: se declara automáticamente cuando el valor absoluto de totales.importeTotal alcanza 100.000.000 €.
Destinatario extranjero
Un destinatario sin NIF español se identifica con destinatario.idOtro en lugar de nif. Enviar nif e idOtro a la vez devuelve 400.
"destinatario": {
"nombre": "Kunde GmbH",
"idOtro": { "codigoPais": "DE", "idType": "02", "id": "DE123456789" }
}
| Campo | Regla |
|---|---|
idOtro.idType |
02 NIF-IVA, 03 pasaporte, 04 documento oficial del país de residencia, 05 certificado de residencia, 06 otro documento probatorio, 07 no censado. |
idOtro.codigoPais |
Código ISO 3166-1 alfa-2. Opcional con 02. Obligatorio ES con 07. Con ES, solo se admiten 03 y 07. |
idOtro.id |
Solo letras y dígitos, de 1 a 20 caracteres. Espacios, puntos, guiones u otros símbolos devuelven 400. |
El formato de identificación de cada país lo comprueba la AEAT: un id que no corresponde al país vuelve como registro rechazado.
Una entrega intracomunitaria exenta (operacionExenta: "E5" con IVA) exige un destinatario con idOtro.
Claves por impuesto
Las claves admitidas en cada línea de detalleDesglose dependen de impuesto. Una claveRegimen fuera de la lista de su impuesto devuelve 400.
impuesto |
claveRegimen |
operacionExenta |
|---|---|---|
01 IVA |
Obligatoria: 01–11, 14, 15, 17–20. |
E1–E6. |
03 IGIC (Canarias) |
Obligatoria: 01–11, 14, 15, 17–21. |
E1–E8. |
02 IPSI (Ceuta y Melilla) |
Opcional: 01, 08, 11, 18, 19, 20. Si se omite, se transmite 01. |
E1–E6. |
05 Otros |
No se admite. Enviarla devuelve 400. |
E1–E6. |
Un mismo código puede significar cosas distintas según el impuesto: 17 es comerciante minorista en IGIC y OSS/IOSS en IVA.
operacionExenta sustituye a calificacionOperacion: la línea lleva una de las dos, nunca ambas, y una línea exenta no lleva tipo ni cuota.
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.
{
"nifEmisor": "B00000000",
"idFactura": "…",
"estado": "pendiente_proceso",
"numSerieFactura": "A12345",
"serie": "A",
"numero": "12345",
"tipoFactura": "F1",
"importeTotal": "121.00",
"cuotaTotal": "21.00",
"creadoEn": "2026-04-03T12:00:00Z",
"avisos": [
"El endpoint /subsanar establece automáticamente subsanacion=true (valor recibido: 'N'). Si necesitas control explícito sobre este campo, usa el endpoint /crear."
],
"urlValidacion": "https://…",
"qrBase64": "…"
}
La respuesta tiene la misma forma que la del alta. avisos aparece cuando el cuerpo no traía subsanacion: true: la API lo ha fijado por ti y lo dice.
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: tiene tres estados.
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.
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": "B00000000",
"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": "B00000000",
"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": "B00000000",
"serie": "A",
"numero": "12345",
"fechaExpedicion": "03-04-2026"
}
}),
});
if (!respuesta.ok) throw new Error(`VeriBai ${respuesta.status}`);
const datos = await respuesta.json();
{
"code": "SUCCESS",
"message": "Anulación registrada correctamente",
"data": {
"nifEmisor": "B00000000",
"idFactura": "…",
"estado": "pendiente_envio",
"numSerieFactura": "A12345",
"serie": "A",
"numero": "12345",
"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. …"
}
}
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.
| 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. |
facturaAnulada.nombreEmisor |
Opcional. Nombre o razón social del emisor tal como figura en el censo de la AEAT. Solo hace falta cuando VeriBai no tiene el alta de la factura; si la tiene, se ignora. Ver el nombre del emisor. |
refExterna |
Opcional. Referencia interna. |
sinRegistroPrevio |
Opcional, booleano (false por defecto). true declara que el alta no 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. |
La anulación genera su propio registro encadenado: nada desaparece de la cadena.
Repetir una anulación
Una anulación repetida es un reintento, no una segunda anulación. Si la factura ya tiene una anulación, en cualquier estado salvo rechazada, la respuesta es 200 SUCCESS con esa anulación y data.yaAnulada: true: data.creadoEn y data.estado son los de la anulación existente. No se crea ni se envía nada a la AEAT.
Si la AEAT rechazó la anulación anterior, la petición se acepta como anulación nueva.
El nombre del emisor en la anulación
La AEAT comprueba el NIF y el nombre del emisor juntos; en una persona física, contra el censo. El registro de anulación no lleva nombre, así que la API envía el que declaró el alta en emisor.nombre. Cuando VeriBai tiene ese alta, facturaAnulada.nombreEmisor no hace falta y se ignora si lo envías.
Para una factura que VeriBai no ha visto, normalmente con sinRegistroPrevio: true, envía facturaAnulada.nombreEmisor tal como figura en el censo de la AEAT. Sin él, la respuesta es 400 VALIDATION_ERROR con una entrada en errors que empieza por facturaAnulada.nombreEmisor:, y no se envía nada a la AEAT.
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. En crear, también una fecha mal formada en rectificativa.facturasRectificadas[i].fechaExpedicion. En anular, también cuando falta facturaAnulada.nombreEmisor para una factura sin alta en VeriBai: la entrada empieza por facturaAnulada.nombreEmisor:. |
400 R3_TIMING_ERROR |
crear: rectificativa R3 con menos de seis meses desde la factura que rectifica. Ver Rectificar y anular. |
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 INVOICE_HAS_RECTIFICATIVAS |
anular: la factura tiene rectificativas. Anula primero las rectificativas; details.rectificativas las lista. |
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. |