VeriBaiDocs
Acceder

Cliente de Python

El paquete oficial de Python, con entornos explícitos, reintentos seguros, veredictos y errores tipados.

La API es HTTP y no exige ningún SDK. El cliente de Python existe para lo que no cabe en una petición suelta: decidir cuándo un reintento es seguro, esperar el veredicto de la administración y poner los importes en el cable sin que pasen por un float.

pip install veribai

Requiere Python 3.10 o superior. Una sola dependencia en tiempo de ejecución, requests.

La llamada concreta de cada endpoint está en la Referencia API, en la pestaña Python de cada ejemplo. Esta página no la repite: cubre lo que atraviesa todas las llamadas.

Qué añade sobre requests

  • Reintentos que distinguen un rechazo de un resultado desconocido. Ver Reintentos.
  • Importes y fechas en el formato exacto que exige la API. Pasa Decimal y date; la conversión se hace al salir. El float se rechaza de plano, porque 0.1 no es exactamente 0.1 en coma flotante binaria y un céntimo de desviación en una cuota es un defecto fiscal. Tampoco se redondea en silencio: un importe con más precisión de la que admite el formato levanta una excepción, y la decisión de redondear sigue siendo tuya.
  • Verificación de webhooks sobre los bytes crudos, que es el único sitio donde la firma sigue siendo válida.
  • Errores tipados sobre los que se puede ramificar, cada uno con el code de la API.

Lo que no hace es replicar las reglas de validación fiscal. Las fijan cuatro administraciones y cambian. Un cliente que rechazara en local se quedaría desactualizado y empezaría a rechazar facturas que la API habría aceptado. Aquí se comprueba el formato; la legalidad la decide la API.

Entornos

Pasa el entorno explícito, siempre:

import veribai

client = veribai.Client(api_key="...", environment="test")   # sandbox
client = veribai.Client(api_key="...", environment="live")   # producción

El entorno se resuelve en este orden, y solo en este orden:

  1. el argumento environment=
  2. la variable VERIBAI_ENVIRONMENT
  3. test

Un argumento explícito gana siempre a la variable, y eso es justo lo que hace que merezca la pena escribirlo. Un constructor sin environment= no es sandbox: es sandbox mientras esa variable no esté definida. Un despliegue que la define en live convierte en producción un código que no menciona producción por ninguna parte.

Si pasas tu propia variable, no la dejes caer a cadena vacía:

# ValueError: unknown environment ''
veribai.Client(api_key="...", environment=os.environ.get("MI_ENTORNO", ""))

Falla en el constructor, que es donde quieres enterarte.

Un entorno equivocado no es una factura equivocada

Las claves se emiten por entorno. Una clave TEST contra api.veribai.com, o una clave LIVE contra sandbox.veribai.com, la rechaza la pasarela con un 403 sin campo code, que el cliente lanza como AuthenticationError. Un environment= mal puesto se manifiesta como un error de autenticación, no como un registro presentado donde no tocaba.

La comprobación autorizada es client.cuenta.obtener(): el entorno que devuelve lo deduce la API de la propia clave, no del argumento que le pasaste. Es la llamada que conviene dejar en el arranque de la aplicación.

La API de gestión no recibe ningún parámetro de entorno. Es una sola pasarela y lo resuelve a partir de la clave.

Sin api_key, el cliente lee VERIBAI_API_KEY. Detalle de claves y rotación en Autenticación.

Reintentos

Un 429, o un 503 que la API documenta como seguro, demuestran que el servidor rechazó la petición antes de hacer nada. Reintentar es seguro siempre. Una conexión caída no demuestra nada: la factura puede estar ya firmada y encolada.

Por eso los errores de red se reintentan solo en las rutas que la API reproduce por identidad. El alta es idempotente por serie, número y fecha de expedición: un reenvío idéntico devuelve el registro original en lugar de crear un segundo. Esa identidad es del servidor, y es lo que hace seguro el reintento, así que el cliente no inventa ninguna clave de idempotencia propia. Cambia el importeTotal entre dos intentos y la respuesta es 409 INVOICE_IDENTITY_CONFLICT, que es lo correcto: es otra factura con el mismo número.

En las rutas donde un duplicado sería un segundo objeto real, como crear un webhook o registrar un dispositivo, un error de red no se reintenta nunca.

La política se ajusta en el constructor:

client = veribai.Client(
    api_key="...",
    environment="test",
    retry=veribai.RetryPolicy(max_attempts=4, backoff_base=0.5, backoff_max=20.0),
    timeout=30.0,
)

max_attempts=1 desactiva los reintentos. Se respeta Retry-After cuando el servidor lo envía, y el jitter está activo para que una flota de workers no se resincronice en el mismo segundo después de un throttle.

Qué devuelve cada código y cuál es reintentable, en Errores. Los límites y la cuota, en Límites.

Del 200 al veredicto

Un 200 de crear significa aceptada, no presentada. VeriBai ha recibido la factura, la ha validado y, en TicketBAI, ya la ha firmado. La administración responde después, y esa respuesta es la que tiene valor.

Hay dos momentos, no uno:

respuesta = client.verifactu.crear(factura)

verdicto = client.facturas.esperar_verdicto(
    respuesta["idFactura"], nif_emisor="B76116342", con_detalle=True
)

if verdicto.registrada:
    ...
elif verdicto.rechazada:
    # No presentada. La obligación sigue abierta: corrige y subsana.
    ...
elif verdicto.requiere_subsanacion:
    # Presentada, pero con errores. No se va a resolver sola.
    ...

esperar_verdicto sondea hasta que hay respuesta. con_detalle añade una llamada más, solo en el sondeo final, para que un rechazo llegue con el código y la descripción de la propia administración en verdicto.detalle.

aceptada_con_errores es un estado terminal, aunque parezca progreso. El registro está presentado y no va a cambiar solo. Lo sustituye una subsanación que envíes tú. Ver Rectificar y anular.

Si se agota el plazo, el cliente levanta VerdictTimeout. No es un fallo de la factura: la administración no había respondido todavía, y ultimo_estado guarda la última lectura para reanudar el sondeo más tarde.

Cada sondeo es una llamada contra la cuota mensual, que es por clave. En cuanto haya volumen, un webhook sustituye al sondeo.

Errores

Cada fallo documentado tiene su excepción, y todas llevan el code de la API.

try:
    client.verifactu.crear(factura)
except veribai.IdentityConflictError as exc:
    # misma serie, número y fecha, distinto importe: es otra factura
    print(exc.code, exc.errors)
except veribai.ValidationError as exc:
    print(exc.errors)          # cadenas por campo, incluidos códigos de la AEAT
except veribai.RateLimitError as exc:
    print(exc.retry_after)     # ya se reintentó; esto es al agotar la política
except veribai.APIError as exc:
    print(exc.status, exc.code, exc.request_id)

Ramifica por code, nunca por el estado HTTP a secas. Un 409 es un conflicto de identidad en crear, ALREADY_CANCELLED en anular y un límite de plan en clientes/crear.

exc.errors es prosa legible por humanos, y a veces trae códigos de reglas de la AEAT dentro. Sirve para enseñarla o registrarla, no para parsearla.

exc.request_id es el identificador de la pasarela. Cítalo si escribes a soporte.

Un 403 con el cuerpo {"message": "Forbidden"} y sin campo code lo emite la pasarela, no la API: clave ausente, desconocida, deshabilitada o de otro entorno. Llega como AuthenticationError, para que no lo busques donde no está.

Todas heredan de veribai.VeriBaiError. La tabla completa de códigos, en Errores.

Paginación

listar devuelve una página. iterar las recorre todas:

for factura in client.facturas.iterar("B76116342", estado="registrada"):
    print(factura["numeroFactura"])

Una Pagina se itera, tiene len() y expone hay_mas y proxima_pagina. El cursor es opaco y va ligado a tu cuenta: devuélvelo sin tocarlo. Uno alterado o truncado es un 400 INVALID_CURSOR, y se arregla empezando por la primera página, nunca reparándolo.

total_en_pagina cuenta las filas de esa página, no el total del emisor.

Una página es una llamada. iterar acepta max_paginas como barrera, porque un bucle sin límite sobre una API paginada es la forma clásica de agotar una cuota que es por clave.

Webhooks

El HMAC se calcula sobre los bytes crudos de la petición, así que el json.loads que hace tu framework antes de darte el cuerpo ya lo ha invalidado. veribai.webhooks.parse_entrega verifica primero, parsea después y devuelve el identificador de entrega sobre el que deduplicar.

El receptor completo, con la verificación y el tratamiento de duplicados, en Webhooks.

El QR

client.facturas.qr() devuelve los bytes del PNG.

La pasarela solo entrega el binario cuando la petición envía Accept: image/png. Con el */* que mandan por defecto la mayoría de clientes HTTP, el cuerpo es el texto base64 del PNG aunque siga anunciándose como image/png: lo escribes a un fichero y obtienes una imagen que no abre. El cliente manda siempre la cabecera y, aun así, comprueba el número mágico del PNG antes de devolverlo.

guardar_qr hace lo mismo y lo escribe en la ruta que le des.