Facturas y registros
Los endpoints de lectura. Estado, detalle, QR, XML firmado y listados.
La remisión a Hacienda es asíncrona. Estos endpoints permiten seguir cada factura hasta su estado final y recuperar sus artefactos: el QR, el XML firmado y el historial de registros. Base URL: https://sandbox.veribai.com (TEST) o https://api.veribai.com (LIVE).
| Método | Endpoint | Descripción |
|---|---|---|
GET |
/v1/facturas |
Listado de facturas de un emisor. |
GET |
/v1/facturas/buscar |
Localiza una factura por serie, número y fecha. Recupera su idFactura. |
GET |
/v1/facturas/{idFactura} |
Detalle completo de una factura. |
GET |
/v1/facturas/{idFactura}/estado |
Estado actual (polling). |
GET |
/v1/facturas/{idFactura}/qr |
QR de la factura (PNG binario). |
GET |
/v1/facturas/{idFactura}/xml |
XML firmado del registro. |
GET |
/v1/registros |
Listado de registros de facturación de un emisor. |
GET |
/v1/registros/{idRegistro} |
Detalle de un registro. |
{idFactura} es el idFactura que devuelve el alta. Todas las lecturas requieren el parámetro nifEmisor (el NIF del emisor) y verifican que pertenece a tu cuenta; un emisor ajeno devuelve 403.
GET /v1/facturas
curl "https://sandbox.veribai.com/v1/facturas?nifEmisor=B98765432&limite=100" \
-H "x-api-key: TU_CLAVE"
import veribai
client = veribai.Client(api_key="TU_CLAVE", environment="test")
pagina = client.facturas.listar("B98765432", limite=100)
for factura in pagina:
print(factura["numeroFactura"])
const respuesta = await fetch("https://sandbox.veribai.com/v1/facturas?nifEmisor=B98765432&limite=100", {
method: "GET",
headers: { "x-api-key": "TU_CLAVE" },
});
if (!respuesta.ok) throw new Error(`VeriBai ${respuesta.status}`);
const datos = await respuesta.json();
| Parámetro | Notas |
|---|---|
nifEmisor |
Obligatorio. NIF del emisor cuyas facturas quieres listar. |
fechaInicio / fechaFin |
Opcionales. Acotan por fecha de registro, en ISO 8601. |
estado |
Opcional. Filtro por ciclo de vida: registrada, anulada o rectificada. |
sistemaFiscal |
Opcional: verifactu o ticketbai. |
limite |
Opcional, por defecto 100, máximo 500. Un valor por encima del máximo se recorta a 500; uno no entero o menor que 1 responde 400 INVALID_PARAMETER. ?limite= vacío usa el valor por defecto. |
cursor |
Opcional. Cursor de paginación: el proximaPagina de la página anterior. |
{
"facturas": [
{
"idFactura": "…",
"nifEmisor": "B98765432",
"numeroFactura": "A12345",
"tipoFactura": "F1",
"importeTotal": "121.00",
"cuotaTotal": "21.00",
"sistemaFiscal": "verifactu",
"estadoFactura": "registrada",
"fechaRegistro": "2026-04-03T10:01:03Z",
"ultimaModificacion": "2026-04-03T10:01:03Z",
"urlQr": "https://…",
"hashVerifactu": "…",
"csvAeat": "…"
}
],
"total": 15,
"nifEmisor": "B98765432",
"nombreEmisor": "Restaurant La Buena Mesa SL",
"proximaPagina": null
}
proximaPagina trae el cursor de la página siguiente; null significa que no hay más páginas. Los campos sin valor se omiten, y filtros aparece solo cuando la petición incluyó algún filtro.
GET /v1/facturas/{idFactura}/estado
El endpoint de polling: mínimo, estable y pensado para consultarse de forma diferida.
curl "https://sandbox.veribai.com/v1/facturas/ID_FACTURA/estado?nifEmisor=B98765432" \
-H "x-api-key: TU_CLAVE"
import veribai
client = veribai.Client(api_key="TU_CLAVE", environment="test")
respuesta = client.facturas.estado("ID_FACTURA", nif_emisor="B98765432")
const respuesta = await fetch("https://sandbox.veribai.com/v1/facturas/ID_FACTURA/estado?nifEmisor=B98765432", {
method: "GET",
headers: { "x-api-key": "TU_CLAVE" },
});
if (!respuesta.ok) throw new Error(`VeriBai ${respuesta.status}`);
const datos = await respuesta.json();
Mientras la remisión está en curso, la respuesta trae el estado del envío:
{
"idFactura": "…",
"numeroFactura": "A12345",
"estadoEnvio": "en_cola",
"estadoFactura": null,
"sistemaFiscal": "ticketbai",
"ultimaModificacion": "2026-04-03T10:00:12Z"
}
Cuando la administración acepta el registro, estadoFactura toma el relevo:
{
"idFactura": "…",
"numeroFactura": "A12345",
"estadoFactura": "registrada",
"csvAeat": "…",
"sistemaFiscal": "verifactu",
"ultimaModificacion": "2026-04-03T10:01:03Z"
}
| Campo | Notas |
|---|---|
estadoEnvio |
Estado de la remisión en curso: pendiente_proceso, pendiente_envio, en_lote, en_cola, rechazada… (desconocido si el registro no informa estado). Presente mientras la factura no está registrada. |
estadoFactura |
Estado de la factura ya registrada: registrada, anulada o rectificada. null mientras la remisión no ha concluido. |
csvAeat |
VeriFactu: el CSV que la AEAT asigna al registro aceptado. |
Un estadoEnvio de rechazada sin estadoFactura significa rechazo. Consulta el detalle y actúa según el caso: rectificar o subsanar.
Si prefieres no sondear, los webhooks llaman a tu servidor cuando la factura alcanza su desenlace: registrada, rechazada o anulada.
GET /v1/facturas/{idFactura} (detalle)
Devuelve la factura completa: datos de emisión, estado, artefactos y trazabilidad, en el sobre { "factura": {…}, "registros": […], "totalRegistros": N }. Los campos concretos dependen del sistema (VeriFactu o TicketBAI) y del estado; mientras la factura sigue en proceso, la vista mínima devuelve estadoFactura: "procesando". Requiere ?nifEmisor=.
GET /v1/facturas/buscar
Localiza una factura a partir de la identidad que ya tienes (serie, número y fecha de expedición) cuando has perdido su idFactura. Sin ese identificador quedas fuera también de /estado, /qr, /xml y /registros.
curl "https://sandbox.veribai.com/v1/facturas/buscar?nifEmisor=B98765432&serie=A&numero=12&fechaExpedicion=15-01-2026" \
-H "x-api-key: TU_CLAVE"
from datetime import date
import veribai
client = veribai.Client(api_key="TU_CLAVE", environment="test")
respuesta = client.facturas.buscar(
nif_emisor="B98765432",
numero="12",
fecha_expedicion=date(2026, 1, 15),
serie="A",
)
const respuesta = await fetch("https://sandbox.veribai.com/v1/facturas/buscar?nifEmisor=B98765432&serie=A&numero=12&fechaExpedicion=15-01-2026", {
method: "GET",
headers: { "x-api-key": "TU_CLAVE" },
});
if (!respuesta.ok) throw new Error(`VeriBai ${respuesta.status}`);
const datos = await respuesta.json();
| Parámetro | Notas |
|---|---|
nifEmisor |
Obligatorio. NIF del emisor. |
numero |
Obligatorio. El número de factura sin la serie. |
fechaExpedicion |
Obligatorio. dd-mm-aaaa, con dos dígitos en día y mes. |
serie |
Opcional. Omítelo si la factura se emitió sin serie. |
Devuelve el mismo sobre que el detalle ({ "factura": {…}, "registros": […], "totalRegistros": N }), así que la respuesta trae el idFactura y con él vuelves a tener acceso a /estado, /qr, /xml y /registros.
serieynumerovan separados, nunca unidos. El camponumeroFacturade las respuestas es la concatenación de ambos:serie=A&numero=12yserie=A1&numero=2se leen igual ahí (A12), pero son facturas distintas. Si envías la concatenación ennumero, la búsqueda no encuentra nada y el404resulta inexplicable.
| Error | Cuándo |
|---|---|
400 MISSING_PARAMETER |
Falta numero o fechaExpedicion. |
400 VALIDATION_ERROR |
fechaExpedicion mal formada: 15/01/2026 y 2026-01-15 se rechazan; el formato es 15-01-2026. También si serie o numero superan 60 caracteres. |
404 NOT_FOUND |
Ninguna factura de ese emisor tiene esa identidad. |
Los 400 nombran el parámetro culpable en campo.
QR y XML
GET /v1/facturas/{idFactura}/qr?nifEmisor=… → image/png
GET /v1/facturas/{idFactura}/xml?nifEmisor=… → application/xml
Envía Accept: image/png al pedir el QR. Con el */* que mandan por defecto la mayoría de clientes HTTP, el cuerpo llega como el texto base64 del PNG aunque la respuesta siga anunciándose como image/png, y escribirlo a un fichero produce una imagen que no abre. El cliente de Python manda siempre la cabecera.
El QR también llega en base64 en la respuesta del alta (qrBase64). Este endpoint lo sirve como binario para regeneraciones. El XML firmado es el documento con valor probatorio remitido a la administración: descargable en cualquier momento para auditoría o custodia propia.
El QR se genera en cada petición a partir de la URL de validación que la factura lleva guardada, no se recupera de un almacén de imágenes: es idéntico al qrBase64 que devolvió el alta y está disponible para cualquier factura emitida, sin caducidad.
Ambas rutas responden 404 NOT_FOUND cuando la factura existe pero su documento no está disponible: en /xml, que el documento no está almacenado; en /qr, que la factura no tiene URL de validación, lo que es una anomalía y no un caso corriente.
GET /v1/registros
Facturas y registros son recursos distintos. La factura es la operación; el registro documenta cada remisión: el alta y, si los hubo, su subsanación o anulación. Ver El registro de facturación.
curl "https://sandbox.veribai.com/v1/registros?nifEmisor=B98765432" \
-H "x-api-key: TU_CLAVE"
import veribai
client = veribai.Client(api_key="TU_CLAVE", environment="test")
pagina = client.registros.listar("B98765432")
const respuesta = await fetch("https://sandbox.veribai.com/v1/registros?nifEmisor=B98765432", {
method: "GET",
headers: { "x-api-key": "TU_CLAVE" },
});
if (!respuesta.ok) throw new Error(`VeriBai ${respuesta.status}`);
const datos = await respuesta.json();
| Parámetro | Notas |
|---|---|
nifEmisor |
Obligatorio. NIF del emisor. |
limite / cursor |
Paginación, como en /v1/facturas: por defecto 100, máximo 500, con las mismas reglas de validación. |
Devuelve los registros de alta y de anulación ordenados por fecha de creación descendente, en el sobre { "registros": […], "total", "nifEmisor", "nombreEmisor", "proximaPagina" }.
| Campo | Notas |
|---|---|
idRegistro |
Identificador opaco del registro. Es lo que se envía de vuelta en la ruta del detalle. Para distinguir un alta de una anulación, el campo es tipo. |
tipo |
alta, subsanacion o anulacion. |
estado |
Estado del registro: pendiente al crearse, procesando mientras una corrección VeriFactu está en curso, y aceptada o rechazada al concluir. |
estadoEnvio |
Estado de la remisión mientras está en curso: pendiente_proceso, pendiente_envio, en_lote, en_cola (desconocido si el registro no informa estado). |
estadoEnvioFinal |
El veredicto de la administración: registrada o rechazada. Aparece al concluir la remisión; a partir de ese momento estadoEnvio deja de ser informativo. |
codigoRespuestaAeat / descripcionRespuestaAeat |
El código y la descripción que devolvió la administración. En un rechazo, el motivo. El código viaja como cadena ("1286"), nunca como número. Compáralo como texto. |
csvAeat |
VeriFactu: el CSV asignado al registro aceptado. |
creadoEn / envioCompletadoEn |
Cuándo se creó el registro y cuándo concluyó su remisión. |
idLote |
Lote de envío, cuando el registro viajó agrupado. |
idMaquina |
Presente cuando el envío lo indicó. |
En TicketBAI se añaden tipoRespuestaTbai, codigoRespuestaTbai, mensajeRespuestaTbai, idRegistroTbai y provincia.
GET /v1/registros/{idRegistro} devuelve el detalle de un registro concreto. Requiere ?nifEmisor= y ?idFactura= (la factura a la que pertenece el registro), y añade a los campos anteriores cuotaTotal, sistemaFiscal, ultimaModificacion, esRectificativa, tipoRectificativa y, en los registros de anulación, fechaExpedicion, sinRegistroPrevio y rechazoPrevio.
curl "https://sandbox.veribai.com/v1/registros/UkVDIzIwMjYtMDgtMjRUMDg6Mjk6MzZa?nifEmisor=B98765432&idFactura=ID_FACTURA" \
-H "x-api-key: TU_CLAVE"
import veribai
client = veribai.Client(api_key="TU_CLAVE", environment="test")
respuesta = client.registros.obtener(
"UkVDIzIwMjYtMDgtMjRUMDg6Mjk6MzZa",
nif_emisor="B98765432",
id_factura="ID_FACTURA",
)
const respuesta = await fetch("https://sandbox.veribai.com/v1/registros/UkVDIzIwMjYtMDgtMjRUMDg6Mjk6MzZa?nifEmisor=B98765432&idFactura=ID_FACTURA", {
method: "GET",
headers: { "x-api-key": "TU_CLAVE" },
});
if (!respuesta.ok) throw new Error(`VeriBai ${respuesta.status}`);
const datos = await respuesta.json();
El idRegistro es opaco
Llega en el listado de registros y en el detalle de la factura, y vuelve tal cual: sin recortarlo, sin normalizarlo y sin codificarlo para la URL. No lo interpretes ni lo construyas, y no supongas nada sobre su longitud ni sobre su alfabeto. Su forma es interna y puede cambiar. Guárdalo como una cadena, del mismo modo que el cursor de paginación. Un identificador que no se reconoce responde 400 INVALID_PARAMETER.
La forma anterior, con prefijo REC# o CNC#, ya no se devuelve ni se acepta. Si leías ese prefijo para saber ante qué registro estabas, el campo que lo dice es tipo.
Cómo leer un rechazo
Un registro con estadoEnvioFinal: "rechazada" no ha quedado presentado ante la administración. El motivo está en codigoRespuestaAeat y descripcionRespuestaAeat (en TicketBAI, además, en codigoRespuestaTbai y mensajeRespuestaTbai). Son el código y el texto de la propia administración.
Corrige el dato que señalan y reenvía con PUT /v1/{sistema}/subsanar. VeriBai no reintenta por su cuenta un rechazo por datos: el mismo envío obtendría el mismo rechazo, y en TicketBAI cada intento avanza la cadena de huellas del emisor de forma permanente.
Para no sondear en busca de rechazos, suscríbete al evento
factura.rechazadade los webhooks: llega con el código de la administración en el cuerpo.