VeriBaiDocs
Acceder

TicketBAI

Alta, subsanación y anulación de facturas TicketBAI ante las haciendas forales.

Los endpoints TicketBAI remiten facturas a la hacienda foral que indique provincia. 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/ticketbai/crear Alta de una factura TicketBAI.
PUT /v1/ticketbai/subsanar Subsanación de un registro remitido.
POST /v1/ticketbai/anular Anulación de un registro remitido.

A diferencia de VeriFactu, la firma y el encadenado TicketBAI se completan en el momento del alta: la respuesta ya incluye el identificador TBAI y el QR. La remisión a la hacienda foral se completa en segundo plano.

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/ticketbai/crear

En TicketBAI los datos de identificación viajan en el nivel superior del JSON (no hay bloque cabecera) y el desglose fiscal es la lista plana desglose:

POST/v1/ticketbai/crear

curl -X POST https://sandbox.veribai.com/v1/ticketbai/crear \
  -H "x-api-key: TU_CLAVE" \
  -H "Content-Type: application/json" \
  -d '{
    "version": "1.0",
    "provincia": "bizkaia",
    "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"
    },
    "desglose": [
      {
        "baseImponible": "100.00",
        "tipoImpositivo": "21.00",
        "cuota": "21.00",
        "descripcion": "Consultoría"
      }
    ],
    "importeTotal": "121.00"
  }'

Campos

Campo Notas
provincia Obligatorio: araba, bizkaia o gipuzkoa. Determina la hacienda foral de destino y sus reglas. Debe corresponder con la hacienda del emisor. La discrepancia se rechaza con 400 PROVINCE_MISMATCH.
emisor Obligatorio: nif (9 caracteres) y nombre. El NIF debe corresponder a un cliente dado de alta con la hacienda foral correspondiente.
serie Opcional. TicketBAI admite factura sin serie.
numero Obligatorio.
fechaExpedicion Obligatorio. Formato dd-mm-aaaa; no puede ser posterior a hoy, en hora peninsular. Ver Fechas de la factura.
horaExpedicion Opcional. HH:MM:SS en formato 24 h, dos dígitos por componente. Si se omite, se usa la hora peninsular del momento del alta. Se valida como hora real: 24:00:00 se rechaza.
fechaOperacion Opcional, dd-mm-aaaa, si difiere de la expedición. Cada hacienda foral aplica sus propias reglas. Ver Fechas de la factura.
tipoFactura Obligatorio: F1F4, R1R5. Son los de VeriFactu más F4 (asiento resumen), propio de TicketBAI.
descripcion Descripción de la operación.
destinatario Obligatorio salvo en F2 y R5 (simplificadas), que no lo admiten. nombre más nif o idOtro.
desglose Obligatorio, mínimo una línea: baseImponible, tipoImpositivo (0–100) y cuota, como cadenas decimales. La cuota total se calcula como la suma de las líneas. En simplificadas F2, la suma de baseImponible y cuota de todas las líneas no puede superar los 3.000 €. El tope es exclusivo: 3.000,00 € se acepta y 3.000,01 € devuelve 400 VALIDATION_ERROR nombrando desglose. El recargo de equivalencia queda fuera de esa suma, así que una F2 con recargo puede pasar de 3.000 € de total y seguir siendo válida. Las R5 no tienen tope. El límite es el de la regla AEAT 1150, que recoge el tope de la factura simplificada del art. 4 del RD 1619/2012: ley nacional, no una exigencia foral. Las haciendas forales no lo comprueban; la API sí lo aplica, para que la factura no salga fuera de norma.
importeTotal Importe total de la factura, IVA incluido y sin descontar el IRPF. Ver Retención de IRPF.
rectificativa Obligatorio en tipos R1R5: tipo (S o I) y facturasRectificadas con al menos una referencia (serie, numero, fechaExpedicion).
retencionSoportada Opcional. Retención de IRPF de la factura, como cadena decimal. Se declara aparte: no se resta de importeTotal.
lineas Líneas de detalle de la factura: descripcion, importeUnitario (sin IVA), importeTotal (con IVA); cantidad opcional (por defecto 1) y descuento opcional. Obligatorio en Araba; opcional en Bizkaia y Gipuzkoa. Máximo 250 líneas por factura, en las tres provincias y tanto en crear como en subsanar.
subsanacion / rechazoPrevio Opcionales, booleanos. En la práctica no los envías: PUT /subsanar marca la subsanación 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 y se declara en el documento firmado con sus últimos 30 caracteres.

Tipos y formato del cuerpo

TicketBAI valida el tipo de cada campo antes de construir el XML, para que un dato mal tipado sea un 400 inmediato y no un fallo aguas abajo:

  • provincia, serie, numero, fechaExpedicion, tipoFactura, emisor.nif y emisor.nombre deben ser cadenas JSON. Un número, null, un array o un objeto devuelve 400 VALIDATION_ERROR nombrando el campo (o 400 INVALID_FIELD_TYPE en anular).
  • Las longitudes se comprueban en la entrada, con los máximos del propio XSD de TicketBAI 1.2.2: serie y numero, 20 caracteres cada uno (TextMax20Type); descripcion y lineas[].descripcion, 250; emisor.nombre y destinatario.nombre, 120; destinatario.direccion, 250; destinatario.codigoPostal, 20. Pasarse devuelve 400 VALIDATION_ERROR nombrando el campo y su máximo, sin llegar a firmar.
  • Los caracteres #, & y ' se rechazan en serie y numero: Araba los rechaza con el código 004, y lo hace con el documento ya firmado. /, el espacio, Ñ, -, _, ., <, > y " sí se admiten.
  • Los campos de texto no pueden contener caracteres de control ilegales en XML. El tabulador y los saltos de línea sí se admiten, y se eliminan al serializar.
  • Los campos de texto tampoco admiten entidades XML o HTML ya codificadas (&lt;, &#0;, &amp;…). El escapado del XML lo hace la API, así que ese texto es una doble codificación y devuelve 400 nombrando el campo. Un & suelto dentro de un nombre (Tom & Jerry) es correcto.
  • El cuerpo debe ser un objeto JSON. Un array, una cadena, un número o null devuelven 400 BAD_REQUEST.

Retención de IRPF (autónomos)

Una factura corriente de un autónomo con retención lleva tres importes: base 550,00, IVA 115,50 y retención de IRPF 82,50. El campo importeTotal es el importe con IVA, antes de la retención: aquí "665.50", no "583.00". La retención viaja aparte, en retencionSoportada.

Restar el IRPF de importeTotal es el error habitual, y no pasa silenciosamente: el total deja de cuadrar con el desglose y la petición responde 400, indicando que la retención se declara en su propio campo.

Reglas por provincia

Cada hacienda foral valida cosas distintas. La API las comprueba en el alta, para que el error llegue como un 400 inmediato y no como un rechazo foral diferido:

  • Araba exige las líneas de detalle: lineas es obligatorio.
  • Gipuzkoa exige descripcion en cada línea de desglose.
  • Bizkaia opera dentro de Batuz/LROE (el envoltorio lo gestiona la API sin cambios en tu petición) y solo admite ejercicios desde 2024.

desglose y lineas son cosas distintas: el desglose es el resumen fiscal por tipo impositivo; las líneas son los conceptos de la factura. Ninguno se deriva del otro.

Respuesta

200 OK. La factura queda firmada y encadenada en el momento:

{
  "nifEmisor": "B76116342",
  "idFactura": "…",
  "estado": "en_cola",
  "sistemaFiscal": "ticketbai",
  "provincia": "bizkaia",
  "serieNumero": "A12345",
  "tipoFactura": "F1",
  "importeTotal": "121.00",
  "cuotaTotal": "21.00",
  "creadoEn": "2026-04-03T10:00:00Z",
  "idTbai": "TBAI-B76116342-…",
  "urlValidacion": "https://…",
  "qrBase64": "…"
}
  • idTbai: el identificador TBAI, generado a partir de la firma.
  • estado vale en_cola: firmada y encadenada, pendiente de remisión foral. Estado final en /estado.
  • qrBase64 y urlValidacion: el QR TicketBAI, verificable ante la hacienda foral.

Los duplicados son idempotentes: reenviar un alta con la misma identidad 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 no se incluyen los campos del QR.

La repetición funciona también con la remisión en curso: si el original ya está firmado pero aún no registrado (o fue rechazado), la respuesta trae la identidad almacenada (idTbai, urlValidacion, el estado de la remisión, p. ej. en_cola o rechazada) más "yaExistente": true. La factura nunca se vuelve a firmar, así que el identificador TBAI es estable entre reintentos. En la ventana breve en que una petición idéntica anterior sigue firmando, la respuesta es 409 INVOICE_SIGNING_IN_FLIGHT: reintenta en unos segundos.

PUT /v1/ticketbai/subsanar

Corrige un registro ya remitido. El cuerpo es el mismo que el de crear con los datos corregidos; la API marca la subsanación automáticamente. Acepta rechazoPrevio (booleano) para declarar un rechazo foral previo.

POST /v1/ticketbai/anular

El cuerpo es plano e incluye provincia, como el alta:

POST/v1/ticketbai/anular

curl -X POST https://sandbox.veribai.com/v1/ticketbai/anular \
  -H "x-api-key: TU_CLAVE" \
  -H "Content-Type: application/json" \
  -d '{
    "provincia": "bizkaia",
    "nifEmisor": "B76116342",
    "serie": "A",
    "numero": "12345",
    "fechaExpedicion": "03-04-2026"
  }'
Campo Notas
provincia Obligatorio: araba, bizkaia o gipuzkoa.
nifEmisor Obligatorio. NIF del emisor de la factura a anular.
numero / fechaExpedicion Obligatorios. serie solo si la factura la tenía. Los tres deben coincidir exactamente con la factura original (la fecha forma parte de su identidad).
nombreEmisor Opcional. Por defecto, el nombre registrado del cliente emisor.
idMaquina Opcional.

Respuesta

{
  "nifEmisor": "B76116342",
  "idFactura": "…",
  "idAnulacion": "…",
  "estado": "en_cola",
  "sistemaFiscal": "ticketbai",
  "provincia": "bizkaia",
  "serie": "A",
  "numero": "12345",
  "fechaExpedicion": "03-04-2026",
  "creadoEn": "2026-04-03T11:00:00Z"
}

La anulación se firma y encadena en el momento, como el alta. No genera QR: es un registro de baja, no una factura.

Repetición idempotente: si la factura ya tiene una anulación registrada o en curso, la respuesta es un 200 con la misma forma (sin creadoEn) más "yaAnulada": true; estado refleja el envío de la anulación previa (registrada o en_cola). Una anulación previamente rechazada no bloquea el reintento.

Errores

Los códigos comunes están en Errores. Específicos de estos endpoints:

Código Cuándo
400 INVALID_PROVINCE provincia ausente o distinta de araba, bizkaia, gipuzkoa. En crear/subsanar, el cuerpo incluye provinciaRecibida con el valor recibido.
400 PROVINCE_MISMATCH provincia no coincide con la hacienda registrada del emisor.
400 VALIDATION_ERROR El cuerpo no supera la validación, incluidas las reglas por provincia y los tipos de campo.
400 INVALID_FIELD_TYPE anular: los campos están, pero con el tipo JSON equivocado.
400 MISSING_FIELDS anular: faltan campos obligatorios. El message los enumera.
400 BAD_REQUEST El cuerpo no es un objeto JSON, o el idMaquina tiene formato inválido.
400 R3_TIMING_ERROR crear: rectificativa R3 con menos de seis meses desde la factura que rectifica. Ver Rectificar y anular.
400 INVOICE_HAS_RECTIFICATIVAS No puedes anular una factura con rectificativas: anula primero las rectificativas.
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.
404 INVOICE_NOT_FOUND anular: no existe la factura original para ese emisor.
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.
409 INVOICE_SIGNING_IN_FLIGHT Una petición idéntica anterior sigue completando su firma. Reintenta en unos segundos.
500 XML_PERSIST_ERROR No se pudo guardar un XML tras tres intentos. Si el message dice que no se ha enviado nada (subsanar, anulación corregida), reintenta. En crear la factura quedó firmada y encadenada y el reintento no la desbloquea: escríbenos.
503 CHAIN_CONTENTION La cadena de huellas del emisor se estaba escribiendo simultáneamente. No se firmó ni registró nada: reintenta la misma petición.