Errores
El formato de error, la tabla de códigos y qué reintentar.
Todos los errores de la API comparten el mismo cuerpo:
{
"code": "VALIDATION_ERROR",
"message": "Error de validación",
"errors": [ "cabecera.numero: obligatorio" ]
}
code: código estable, pensado para tratarse por programa. Bifurca tu lógica sobre él: no sobremessage, y no sobre el estado HTTP. Elcodeidentifica la condición concreta; el estado que la acompaña puede afinarse.message: descripción legible, en castellano.errorsaparece solo en errores de validación de cuerpo: la lista de violaciones, campo a campo.campo: en los errores de parámetros de consulta, el parámetro concreto que los provocó (numero,fechaExpedicion,serie).
Un 500 nunca lleva el texto de la excepción interna: el cuerpo se limita a code y message, con una frase fija. Diagnostica por code; si necesitas más contexto, escríbenos indicando la hora de la petición.
Los rechazos que emite la pasarela antes de alcanzar la API no siguen ese formato. El caso que conviene conocer: en los segundos posteriores a rotar una clave, la petición con la clave nueva recibe 403 con el cuerpo {"message": "Forbidden"} y sin code.
Códigos
| HTTP | code |
Cuándo |
|---|---|---|
| 400 | VALIDATION_ERROR |
El cuerpo no supera la validación. errors enumera los campos. |
| 400 | INVALID_JSON |
El cuerpo no es JSON válido. |
| 400 | MISSING_PARAMETER / INVALID_PARAMETER |
Parámetro ausente o inválido: de consulta (nifEmisor, limite) o de ruta (un idRegistro que no se reconoce). |
| 400 | INVALID_PROVINCE |
TicketBAI: provincia ausente o distinta de araba, bizkaia, gipuzkoa. |
| 400 | PROVINCE_MISMATCH |
TicketBAI: provincia no coincide con la hacienda registrada del emisor. |
| 400 | NOT_APPLICABLE_FOR_TAX_AGENCY |
Representación no disponible para esa hacienda. Hoy las cuatro tienen documento gestionado, así que en la práctica solo aparece si la hacienda del emisor falta o no se reconoce. |
| 400 | MISSING_FIELDS |
Faltan campos obligatorios en el cuerpo. El message los enumera. |
| 400 | INVALID_FIELD_TYPE |
Los campos están presentes pero con el tipo JSON equivocado (p. ej. nifEmisor como objeto). errors nombra cada uno. TicketBAI anular. |
| 400 | BAD_REQUEST |
El cuerpo no es un objeto JSON: un array, cadena, número o null se rechaza aquí y no más adelante. También cubre un idMaquina con formato inválido. |
| 400 | TAX_SYSTEM_MISMATCH |
El emisor está registrado para el otro sistema fiscal: usa el endpoint que corresponde a su hacienda. |
| 400 | RECTIFICATIVA_ERROR |
El bloque rectificativa es inválido o no concuerda con el tipoFactura (R1–R5). |
| 400 | R3_TIMING_ERROR |
Una rectificativa R3 (art. 80.4 LIVA) con menos de seis meses desde la factura que rectifica. Los dos sistemas. details incluye fechaOriginal, fechaRectificativa y mesesTranscurridos. Ver Rectificar y anular. |
| 400 | DATE_FORMAT_ERROR |
VeriFactu crear, solo en R3: una de las fechas que compara la regla de los seis meses no pudo interpretarse (DD-MM-YYYY). En TicketBAI el mismo caso responde RECTIFICATIVA_ERROR. |
| 400 | MISSING_BODY |
VeriFactu anular: la petición no lleva cuerpo. |
| 400 | INVALID_BODY / INVALID_PDF / INVALID_CERT / CERT_ERROR / SIGNING_ERROR |
Representación: cuerpo que no se decodifica, PDF o certificado que no son base64, certificado que no carga, o firma que falla. |
| 400 | SIGNATURE_* / NO_GENERATION_RECORD |
verificar rechazó el documento firmado. El código nombra la causa y detallesVerificacion trae el diagnóstico. Tabla completa. |
| 401 | UNAUTHORIZED |
Clave API ausente, inválida o revocada. |
| 402 | PAYMENT_REQUIRED |
Facturación de la cuenta suspendida. Solo endpoints de escritura. |
| 403 | FORBIDDEN |
El recurso pertenece a otra cuenta, o tu plan no incluye ese entorno. |
| 403 | UNAUTHORIZED_NIF |
El NIF emisor no está dado de alta bajo tu cuenta. |
| 403 | REPRESENTATION_PENDING |
Solo LIVE: el emisor no tiene la representación firmada. |
| 404 | NOT_FOUND |
El recurso no existe. En /v1/facturas/buscar, que ninguna factura de ese emisor tiene esa identidad. |
| 404 | NOT_UPLOADED_YET |
Declaración Responsable: todavía no hay documento publicado. |
| 404 | INVOICE_NOT_FOUND |
TicketBAI anular: no existe la factura original para ese emisor. |
| 409 | INVOICE_NOT_FOUND |
VeriFactu anular: enviaste sinRegistroPrevio: false pero la factura no consta. Si nunca se reportó, usa true. |
| 409 | INVOICE_EXISTS |
VeriFactu anular: el caso inverso. Enviaste sinRegistroPrevio: true pero la factura sí consta. |
| 409 | ALREADY_CANCELLED |
VeriFactu anular: ya existe una anulación para esa factura. Una anulación que se registró pero nunca llegó a procesarse no cuenta: el reintento la reencola. (En TicketBAI la repetición responde 200 con yaAnulada: true.) |
| 409 (VeriFactu) / 400 (TicketBAI) | INVOICE_HAS_RECTIFICATIVAS |
No puedes anular una factura que tiene rectificativas: anula primero las rectificativas. El cuerpo incluye details.rectificativas. |
| 409 | DUPLICATE_INVOICE |
Caso límite: el registro duplicado existe pero sus datos no pudieron recuperarse. El duplicado normal responde 200 (repetición idempotente), nunca 409. |
| 409 | CLIENT_LIMIT_REACHED |
El alta de un cliente (o la reactivación de uno inactivo) superaría el límite de emisores del plan. La petición es correcta: es el estado de la cuenta el que choca con ella. |
| 409 | SELF_CLIENT_PROTECTED |
El emisor que es tu propia empresa no puede desactivarse ni eliminarse: solo se factura para un emisor activo. |
| 409 | SELF_CLIENT_CONFLICT |
El alta lleva tu propio NIF y ya está registrado en otra cuenta. Con el NIF de un tercero el mismo caso responde 400 VALIDATION_ERROR, a propósito. |
| 409 | SIGNING_IN_PROGRESS |
PATCH de un cliente mientras hay una firma de representación en curso, sobre campos que alimentan el documento. camposBloqueados los enumera. |
| 409 | CLIENT_DELETED |
PATCH /v1/clientes/{nif}/estado sobre un emisor eliminado. Se restaura desde el panel, que es lo único que comprueba la ventana de 30 días. |
| 409 | CLIENT_STATUS_INVALID |
El estado guardado del emisor no admite ese cambio. Escríbenos. |
| 409 | INVOICE_SIGNING_IN_FLIGHT |
TicketBAI crear: una petición idéntica anterior aún está completando su firma. Reintenta en unos segundos. El reintento devuelve la identidad ya almacenada. Si persiste después de un RECORD_PERSIST_ERROR o de un XML_PERSIST_ERROR en crear, escríbenos: el documento firmado no llegó a guardarse. |
| 410 | GONE |
El recurso fue eliminado y ha pasado su ventana de restauración de 30 días. |
| 429 | Ninguno | Límite de peticiones o cuota mensual superados. Ver Límites. |
| 500 | INTERNAL_ERROR |
Fallo inesperado. Reintenta; si persiste, escríbenos. |
| 500 | AWS_SERVICE_ERROR |
Falló una dependencia de infraestructura. Siempre es seguro reintentar: se devuelve antes de firmar, encadenar o persistir nada. |
| 500 | QR_GENERATION_ERROR |
VeriFactu crear: no se pudo generar el QR obligatorio. No se registró nada: puedes reintentar. |
| 500 | ENQUEUE_ERROR |
crear/anular: el registro no pudo encolarse para su procesamiento. La petición se deshace por completo, así que el reintento arranca limpio. |
| 500 | RECORD_PERSIST_ERROR |
TicketBAI crear/anular: la factura quedó firmada y encadenada, pero su registro de remisión no pudo escribirse, así que no se envió a la hacienda foral. Reintenta la misma petición: recupera la factura ya firmada (mismo identificador, mismo QR, sin volver a firmarla) y la envía, respondiendo 200 con yaExistente: true. |
| 500 | PROPAGATION_INCOMPLETE |
PATCH de un webhook: el webhook se actualizó, pero algún emisor vinculado conserva los valores anteriores. Repite el mismo PATCH. Es idempotente. |
| 500 | XML_PERSIST_ERROR |
TicketBAI: no se pudo guardar un XML tras tres intentos. Lee el message. Si dice que no se ha enviado nada (subsanar y la anulación corregida), nada se firmó ni se encoló y el reintento es seguro. En crear («Error al guardar el XML firmado de TicketBAI») la factura sí quedó firmada y encadenada, y el reintento responde 409 INVOICE_SIGNING_IN_FLIGHT indefinidamente: no insistas, escríbenos. |
| 500 | DATABASE_ERROR |
Falló una operación de base de datos en los endpoints de consulta o de gestión. Reintentable. |
| 500 | AUTH_ERROR |
VeriFactu anular: falló el propio paso de autorización. No es una denegación. Reintentable. |
| 500 | CF_NOT_CONFIGURED |
Declaración Responsable: fallo de configuración del servidor, no de tu petición. Escríbenos. |
| 503 | AEAT_UNAVAILABLE |
El censo de la AEAT no se pudo alcanzar (validación de NIF). El cuerpo puede incluir las entradas que sí pudieron resolverse. Un NIF que la AEAT omite en su respuesta no es este error: vuelve en un 200 como no_procesado. |
| 503 | CHAIN_CONTENTION |
TicketBAI: la cadena de huellas del emisor estaba siendo escrita simultáneamente. No se firmó ni registró nada: reintenta la misma petición. |
Qué reintentar
El cliente de Python aplica esta política por ti, incluida la distinción entre un rechazo del servidor y una conexión caída. Lo que sigue es lo que hay que implementar si llamas a la API en crudo.
429: espera y reintenta con retroceso exponencial. Si es por cuota mensual, el reintento inmediato no ayuda.500y503: reintentables. La creación de facturas es idempotente por (emisor, serie, número, fecha): reintentar un alta cuya primera petición sí llegó devuelve200con el registro ya existente, no un duplicado.RECORD_PERSIST_ERROR: reintenta la misma petición. La factura está firmada pero no remitida, y el reintento la recupera y la envía sin volver a firmarla: no se duplica ni avanza la cadena de huellas. Es el único500cuyo reintento responde200conyaExistente: true.AWS_SERVICE_ERROR,ENQUEUE_ERROR,QR_GENERATION_ERRORyCHAIN_CONTENTION: reintentables sin reservas. Los cuatro se devuelven antes de firmar y encadenar, o deshacen por completo lo escrito: no dejan a medias ningún registro ni consumen posición en la cadena de huellas.DATABASE_ERRORyAUTH_ERROR: reintentables.XML_PERSIST_ERROR: depende delmessage. Si dice que no se ha enviado nada, reintenta. Si es el decrear, no: la factura está firmada y encadenada, y hace falta intervención de soporte.- Resto de
4xx: no reintentes sin corregir la petición, porque el resultado será el mismo.
Cuentas multi-TPV
Los emisores con Alta capacidad contratada, donde cada dispositivo lleva su propia cadena de encadenado, tienen códigos propios. Las cuentas estándar no los ven nunca.
En los endpoints de emisión, y solo con el modo ya activo:
| HTTP | code |
Cuándo |
|---|---|---|
| 409 | MACHINE_NOT_REGISTERED |
El idMaquina enviado no está registrado para ese emisor, o está de baja. |
| 409 | SERIE_OWNED_BY_OTHER_MACHINE |
La serie del alta pertenece de forma permanente a otro dispositivo o al envío centralizado. Usa otra serie. |
| 503 | SHARDING_UNAVAILABLE |
La configuración no pudo comprobarse. Reintentable: la petición nunca se encamina sin verificar. |
En los endpoints de gestión de dispositivos: 409 SHARDING_NOT_ENABLED, 409 MACHINE_ALREADY_REGISTERED, 404 MACHINE_NOT_FOUND, 409 CLIENT_NOT_IN_LIVE, 404 SERIE_NOT_RESERVED, 409 RESERVATION_LOCKED, 409 NOT_IN_PREPARATION y 503 ENVIRONMENT_NOT_AVAILABLE.
Si tu volumen lo requiere, habla con el equipo.