Facturas y registros
Los endpoints de lectura. Estado, detalle, QR, XML firmado y listados.
La remisión a la administración 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 (Sandbox) 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 (sondeo). |
GET |
/v1/facturas/{idFactura}/qr |
QR de la factura (PNG en base64). |
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=B00000000&limite=100" \
-H "x-api-key: TU_CLAVE"
import veribai
client = veribai.Client(api_key="TU_CLAVE", environment="test")
pagina = client.facturas.listar("B00000000", limite=100)
for factura in pagina:
print(factura["numSerieFactura"])
const respuesta = await fetch("https://sandbox.veribai.com/v1/facturas?nifEmisor=B00000000&limite=100", {
method: "GET",
headers: { "x-api-key": "TU_CLAVE" },
});
if (!respuesta.ok) throw new Error(`VeriBai ${respuesta.status}`);
const datos = await respuesta.json();
{
"facturas": [
{
"idFactura": "…",
"nifEmisor": "B00000000",
"nifClientePrincipal": "B99999999",
"numSerieFactura": "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",
"idRegistroActual": "UkVDIzIwMjYtMDQtMDNUMTA6MDA6MDBa",
"urlQr": "https://…",
"hashVerifactu": "…",
"csvAeat": "…"
}
],
"total": 15,
"nifEmisor": "B00000000",
"nombreEmisor": "Restaurant La Buena Mesa SL",
"nifClientePrincipal": "B99999999",
"proximaPagina": null
}
| Parámetro | Notas |
|---|---|
nifEmisor |
Obligatorio. NIF del emisor cuyas facturas quieres listar. |
fechaInicio / fechaFin |
Opcionales. Acotan por fecha de registro, en ISO 8601 (2026-01-01T00:00:00Z). Otro formato responde 400 INVALID_DATE_FORMAT. |
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. |
numSerieFactura es la serie y el número concatenados. El listado no incluye serie ni numero por separado: están en el detalle.
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. nifClientePrincipal es el NIF de tu cuenta, e idRegistroActual el registro vigente de la factura. csvAeat es el CSV de ese registro vigente; ver /estado.
GET /v1/facturas/{idFactura}/estado
El endpoint de sondeo: mínimo y estable, pensado para consultas diferidas.
curl "https://sandbox.veribai.com/v1/facturas/ID_FACTURA/estado?nifEmisor=B00000000" \
-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="B00000000")
const respuesta = await fetch("https://sandbox.veribai.com/v1/facturas/ID_FACTURA/estado?nifEmisor=B00000000", {
method: "GET",
headers: { "x-api-key": "TU_CLAVE" },
});
if (!respuesta.ok) throw new Error(`VeriBai ${respuesta.status}`);
const datos = await respuesta.json();
{
"idFactura": "…",
"numSerieFactura": "A12345",
"serie": "A",
"numero": "12345",
"estadoEnvio": "en_cola",
"sistemaFiscal": "ticketbai",
"ultimaModificacion": "2026-04-03T10:00:12Z"
}
Mientras la remisión está en curso, la respuesta trae el estado del envío en estadoEnvio. Cuando la administración acepta el registro, estadoFactura toma el relevo:
{
"idFactura": "…",
"numSerieFactura": "A12345",
"serie": "A",
"numero": "12345",
"estadoFactura": "registrada",
"csvAeat": "…",
"sistemaFiscal": "verifactu",
"ultimaModificacion": "2026-04-03T10:01:03Z"
}
| Campo | Notas |
|---|---|
numSerieFactura |
La serie y el número concatenados (A12345). |
serie / numero |
La serie y el número por separado. Siempre presentes: cadena vacía si la factura no tiene serie. |
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 hasta que la administración acepta un registro de la factura. |
estadoFactura |
Estado de la factura una vez la administración acepta un registro suyo: registrada, anulada o rectificada. Ausente hasta entonces. |
csvAeat |
Solo VeriFactu. El Código Seguro de Verificación del envío con el que la AEAT aceptó el registro vigente de la factura (idRegistroActual). Una subsanación lo sustituye; una anulación no. En una factura sin alta registrada, es el CSV de la anulación aceptada. La AEAT emite un CSV por envío, así que las facturas remitidas en el mismo envío comparten el mismo valor. Las facturas TicketBAI no lo llevan. |
Un estadoEnvio de rechazada sin estadoFactura significa rechazo. Consulta el detalle y actúa según el caso: rectificar o subsanar.
Una factura que VeriBai solo conoce por una anulación, normalmente con sinRegistroPrevio: true porque se expidió antes de usar VeriBai, se consulta igual: estadoEnvio mientras la anulación está en curso o si se rechaza, y estadoFactura: "anulada" cuando la administración la acepta. En VeriFactu, csvAeat trae entonces el CSV de esa anulación, también en el detalle.
Si prefieres no sondear, los webhooks llaman a tu servidor cuando la factura alcanza su estado final: 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. estadoFactura coincide con el de /estado. Mientras la factura no tiene ningún registro aceptado, la vista mínima devuelve estadoFactura: "procesando" si el último registro espera respuesta, y omite estadoFactura si la administración lo rechazó. factura incluye numSerieFactura y, por separado, serie y numero, siempre presentes (cadena vacía si la factura no tiene serie). 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 tampoco puedes llamar a /estado, /qr, /xml ni /registros.
curl "https://sandbox.veribai.com/v1/facturas/buscar?nifEmisor=B00000000&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="B00000000",
numero="12",
fecha_expedicion=date(2026, 1, 15),
serie="A",
)
const respuesta = await fetch("https://sandbox.veribai.com/v1/facturas/buscar?nifEmisor=B00000000&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();
{
"factura": {
"idFactura": "…",
"nifEmisor": "B00000000",
"nifClientePrincipal": "B99999999",
"numSerieFactura": "A12",
"serie": "A",
"numero": "12",
"tipoFactura": "F1",
"importeTotal": "121.00",
"cuotaTotal": "21.00",
"sistemaFiscal": "verifactu",
"estadoFactura": "registrada",
"fechaRegistro": "2026-01-15T10:01:03Z",
"ultimaModificacion": "2026-01-15T10:01:03Z",
"idRegistroActual": "UkVDIzIwMjYtMDEtMTVUMTA6MDA6MDBa",
"urlQr": "https://…",
"hashVerifactu": "…",
"csvAeat": "…"
},
"registros": [
{
"idRegistro": "UkVDIzIwMjYtMDEtMTVUMTA6MDA6MDBa",
"tipo": "alta",
"estado": "aceptada",
"estadoEnvioFinal": "registrada",
"codigoRespuestaAeat": "0",
"csvAeat": "…",
"creadoEn": "2026-01-15T10:00:00Z",
"envioCompletadoEn": "2026-01-15T10:01:03Z",
"idLote": "…"
}
],
"totalRegistros": 1
}
| 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 }), con el idFactura dentro.
serieynumerovan separados. El camponumSerieFacturade 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 responde404.
| 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 afectado en campo.
QR y XML
GET /v1/facturas/{idFactura}/qr?nifEmisor=… → application/json
GET /v1/facturas/{idFactura}/xml?nifEmisor=… → application/xml
El QR responde 200 con un JSON de dos campos, los mismos que trae la respuesta del alta:
{
"qrBase64": "iVBORw0KGgo…",
"urlValidacion": "https://…"
}
qrBase64 es el PNG en base64, sin prefijo data:. urlValidacion es la URL que codifica el QR. Para obtener el fichero, decodifica qrBase64:
curl -s -H "x-api-key: $KEY" \
"https://sandbox.veribai.com/v1/facturas/$ID/qr?nifEmisor=$NIF" \
| jq -r .qrBase64 | base64 -d > qr.png
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 de la factura: 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, una situación anómala.
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=B00000000" \
-H "x-api-key: TU_CLAVE"
import veribai
client = veribai.Client(api_key="TU_CLAVE", environment="test")
pagina = client.registros.listar("B00000000")
const respuesta = await fetch("https://sandbox.veribai.com/v1/registros?nifEmisor=B00000000", {
method: "GET",
headers: { "x-api-key": "TU_CLAVE" },
});
if (!respuesta.ok) throw new Error(`VeriBai ${respuesta.status}`);
const datos = await respuesta.json();
{
"registros": [
{
"idRegistro": "UkVDIzIwMjYtMDQtMDNUMTA6MDA6MDBa",
"tipo": "alta",
"nifEmisor": "B00000000",
"idFactura": "…",
"numSerieFactura": "A12345",
"serie": "A",
"numero": "12345",
"tipoFactura": "F1",
"importeTotal": "121.00",
"estado": "aceptada",
"estadoEnvio": "desconocido",
"creadoEn": "2026-04-03T10:00:00Z",
"estadoEnvioFinal": "registrada",
"codigoRespuestaAeat": "0",
"csvAeat": "…",
"envioCompletadoEn": "2026-04-03T10:01:03Z",
"idLote": "…",
"idMaquina": "CAJA-01"
}
],
"total": 1,
"nifEmisor": "B00000000",
"nombreEmisor": "Restaurant La Buena Mesa SL",
"nifClientePrincipal": "B99999999",
"proximaPagina": null
}
| 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. |
veredictoDesde |
Opcional. Instante ISO 8601 con zona horaria (2026-09-27T10:00:00Z, 2026-09-27T12:00:00+02:00). Solo los registros cuya respuesta de la administración llegó en ese instante o después, de la más antigua a la más reciente. Precisión de segundo, límite inclusivo. Ver Registros por respuesta de la administración. |
estado |
Opcional. aceptada, aceptada_con_errores o rechazada. Solo los registros con esa respuesta. Sin veredictoDesde, de la respuesta más reciente a la más antigua. |
Sin veredictoDesde ni estado, 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. |
numSerieFactura |
La serie y el número de la factura, concatenados. |
serie / numero |
La serie y el número por separado. Siempre presentes: cadena vacía si la factura no tiene serie. |
estado |
Estado del registro. pendiente: sin respuesta todavía; la fase concreta está en estadoEnvio. aceptada: aceptado por la administración. aceptada_con_errores: solo VeriFactu; aceptado con errores que se corrigen con una subsanación, que crea un registro nuevo. Este registro no cambia. rechazada: rechazado por la administración. procesando: excepcional; el resultado no está confirmado. No requiere ninguna acción por tu parte. |
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 |
La respuesta 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"), no como número. Compáralo como texto. |
csvAeat |
Solo VeriFactu. El Código Seguro de Verificación del envío con el que la AEAT aceptó ese registro. Un CSV por envío: los registros remitidos juntos lo comparten. |
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=B00000000&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="B00000000",
id_factura="ID_FACTURA",
)
const respuesta = await fetch("https://sandbox.veribai.com/v1/registros/UkVDIzIwMjYtMDgtMjRUMDg6Mjk6MzZa?nifEmisor=B00000000&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();
{
"registro": {
"idRegistro": "UkVDIzIwMjYtMDgtMjRUMDg6Mjk6MzZa",
"tipo": "alta",
"idFactura": "…",
"nifEmisor": "B00000000",
"numSerieFactura": "A12345",
"serie": "A",
"numero": "12345",
"tipoFactura": "F1",
"importeTotal": "121.00",
"cuotaTotal": "21.00",
"sistemaFiscal": "verifactu",
"estado": "aceptada",
"estadoEnvioFinal": "registrada",
"codigoRespuestaAeat": "0",
"csvAeat": "…",
"creadoEn": "2026-08-24T08:29:36Z",
"ultimaModificacion": "2026-08-24T08:31:02Z",
"envioCompletadoEn": "2026-08-24T08:31:02Z",
"idLote": "…"
}
}
El detalle llega en el sobre { "registro": { … } }.
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.
Registros por respuesta de la administración
Con veredictoDesde o estado, el listado ordena los registros por el momento en que respondió la administración, no por el de su creación. Los webhooks son la vía principal para conocer esa respuesta; este listado recupera las que no llegaron a tu endpoint.
curl "https://sandbox.veribai.com/v1/registros?nifEmisor=B00000000&veredictoDesde=2026-09-27T10:00:00Z" \
-H "x-api-key: TU_CLAVE"
import veribai
client = veribai.Client(api_key="TU_CLAVE", environment="test")
pagina = client.registros.listar(
"B00000000",
veredicto_desde="2026-09-27T10:00:00Z",
)
const respuesta = await fetch("https://sandbox.veribai.com/v1/registros?nifEmisor=B00000000&veredictoDesde=2026-09-27T10:00:00Z", {
method: "GET",
headers: { "x-api-key": "TU_CLAVE" },
});
if (!respuesta.ok) throw new Error(`VeriBai ${respuesta.status}`);
const datos = await respuesta.json();
- Solo aparecen registros con respuesta. Un registro que todavía espera a la administración no está en el listado filtrado. Por eso
estado=pendienteyestado=procesandoresponden400 INVALID_PARAMETER. Para los registros en curso, usa el listado sin filtros o/estado. Excepcionalmente, el listado filtrado porveredictoDesdepuede incluir un registro conestado: "procesando"yestadoEnvio: "desconocido". No es una respuesta final y no requiere ninguna acción por tu parte. - Consulta periódica. Llama con
veredictoDesde=<T>y sigueproximaPaginahasta que valganull. Para la siguiente consulta,<T>es elenvioCompletadoEnmás reciente que recibiste, menos 60 segundos. - Deduplica entre consultas. Cada consulta se solapa un minuto con la anterior, así que los últimos registros de una vuelven al principio de la siguiente. La clave es
idFactura+idRegistro+estado.idRegistrosolo es único dentro de una factura: deduplicar sinidFacturadescarta registros reales. Cada registro recibe una sola respuesta. Una subsanación es un registro nuevo, con su propioidRegistro, y aparece como una entrada más. Para seguir una factura hasta su estado final, agrupa poridFactura. - Limita la memoria de la deduplicación. Si tu proceso guarda las claves ya vistas, después de cada consulta borra las que tengan un
envioCompletadoEnanterior al siguiente<T>: esos registros ya no pueden volver. Así solo guardas el último minuto de registros, aunque el proceso esté activo meses. - Una página puede llegar incompleta o vacía. Con
estado, una página puede traer menos delimiteregistros, incluso ninguno, y aun así traerproximaPagina. Solonullindica que no hay más páginas. - El cursor depende de los filtros. Envía los mismos filtros en cada página. Un
proximaPaginaobtenido con otros filtros responde400 INVALID_CURSOR. - Formato de
veredictoDesde. Sin zona horaria (2026-09-27T10:00:00), solo la fecha o cualquier otro texto responde400 INVALID_PARAMETER. Codifica el+del desfase como%2B, o usaZ.
Cada registro tiene los mismos campos que en el listado sin filtros.
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.