Documentación de la API de Fileverity
Webhooks
Reciba los eventos analysis.completed, analysis.failed y balance.low. Verificación de firmas de Standard Webhooks en JavaScript y Python, reintentos, desactivación automática y el evento de prueba.
Los webhooks envían una solicitud HTTPS a su servidor cuando ocurre algo, para que no tenga que consultar la API periódicamente. Siguen la especificación Standard Webhooks.
Añada endpoints en la Consola, en Webhooks. Elija la URL y los eventos que recibe. El secreto de firma, que empieza por whsec_, se muestra una sola vez al crear el endpoint. Puede cambiar los eventos de un endpoint en cualquier momento sin cambiar su secreto.
Para sustituir un secreto perdido o expuesto, elija Renovar secreto en el endpoint. El secreto nuevo se muestra una sola vez y el anterior deja de funcionar de inmediato, así que actualice enseguida el código con el que verifica las firmas.
- La URL debe usar HTTPS en el puerto predeterminado 443 y apuntar a una dirección pública. Se rechazan las URL con credenciales o con otro puerto.
- Un espacio de trabajo puede tener hasta 10 endpoints.
- Las claves de prueba nunca envían eventos.
Eventos
| Evento | Se envía cuando |
|---|---|
analysis.completed | Un análisis de imagen o de documento se completó correctamente. |
analysis.failed | Un análisis falló o se canceló. No se cobró. Un análisis cancelado tiene status: canceled y error: canceled. |
balance.low | El saldo cayó por debajo del umbral de alerta que fijó en la Consola. |
Los tipos de evento admitidos son analysis.completed, analysis.failed, balance.low.
Contenido de los eventos
Todos los eventos tienen la misma estructura: id, object (siempre event), type, created (marca de tiempo Unix) y data. El id también se envía en el encabezado webhook-id.
analysis.completed y analysis.failed
data es el mismo objeto de análisis que devuelve GET /analyses/{id}, con los mismos nombres de campo y tipos, salvo que result es siempre null: obtenga el resultado con ese endpoint. status es succeeded, failed o canceled, y error es un objeto con code y message, o null. Las marcas de tiempo se expresan en segundos Unix.
{ "id": "evt_Lm8sK3tR6yZ2cXaQ9vB4nP1w", "object": "event", "type": "analysis.completed", "created": 1793700131, "data": { "id": "an_8Hk2pQ4vN9sL1xR7cW3mY6zB", "object": "analysis", "created": 1793700120, "model": "fvimg-2-2026-10-30", "livemode": true, "status": "succeeded", "file": { "name": "claim-photo.jpg", "bytes": 2481920, "format": "jpeg" }, "report_id": "FV-7K2M-9QXD-4T8R", "report_url": "https://fileverity.com/verify/FV-7K2M-9QXD-4T8R", "summary": { "level": "L2", "assessment": "authentic_untouched", "action": "accept", "ai": "very_low", "edit": "none" }, "result": null, "error": null, "progress": null, "usage": { "amount": 199, "currency": "usd", "funding": "balance" }, "metadata": { "order_id": "A-10492" }, "completed_at": 1793700131, "expires_at": 1794304931 }}{ "id": "evt_Zc4vB7nP1sK8tL3wR6yQm2Xa", "object": "event", "type": "analysis.failed", "created": 1793700131, "data": { "id": "an_8Hk2pQ4vN9sL1xR7cW3mY6zB", "object": "analysis", "created": 1793700120, "model": "fvimg-2-2026-10-30", "livemode": true, "status": "failed", "file": { "name": "claim-photo.jpg", "bytes": 2481920, "format": "jpeg" }, "report_id": null, "report_url": null, "summary": null, "result": null, "error": { "code": "analysis_failed", "message": "The analysis could not be completed. You were not charged; retry it, and contact support with the request ID if it fails again." }, "progress": null, "usage": { "amount": 0, "currency": "usd", "funding": null }, "metadata": null, "completed_at": 1793700135, "expires_at": null }}balance.low
balance y threshold están en centavos de dólar estadounidense.
{ "id": "evt_Lm8sK3tR6yZ2cXaQ9vB4nP1w", "object": "event", "type": "balance.low", "created": 1793700500, "data": { "balance": 640, "threshold": 1000 }}Verificar la firma
Cada solicitud incluye tres encabezados, según define la especificación Standard Webhooks:
| Encabezado | Valor |
|---|---|
webhook-id | El ID del evento. Es el mismo en todos los reintentos; úselo para ignorar duplicados. |
webhook-timestamp | Hora Unix, en segundos, de este intento de envío. Cambia en cada reintento. |
webhook-signature | v1, seguido de un HMAC-SHA256 en base64. Un encabezado puede incluir varias firmas separadas por espacios. |
Para verificar una solicitud:
- Lea el cuerpo sin procesar de la solicitud como texto, antes de analizar el JSON.
- Construya el contenido firmado:
webhook-id, un punto,webhook-timestamp, un punto y el cuerpo sin procesar. - Tome su secreto de firma, quite el prefijo
whsec_y decodifique el resto desde base64. Esa es la clave HMAC. - Calcule el HMAC-SHA256 del contenido firmado y codifíquelo en base64.
- Compárelo, en tiempo constante, con cada firma
v1del encabezado. - Rechace las marcas de tiempo que difieran en más de cinco minutos de su reloj.
import { createHmac, timingSafeEqual } from "node:crypto"; export function verify(secret, headers, body) { const id = headers["webhook-id"]; const timestamp = headers["webhook-timestamp"]; const signatures = headers["webhook-signature"]; if (!id || !timestamp || !signatures) throw new Error("Missing webhook headers"); if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) throw new Error("Timestamp outside tolerance"); const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const expected = createHmac("sha256", key).update(id + "." + timestamp + "." + body).digest(); const valid = signatures.split(" ").some((entry) => { const [version, value] = entry.split(","); const given = Buffer.from(value ?? "", "base64"); return version === "v1" && given.length === expected.length && timingSafeEqual(given, expected); }); if (!valid) throw new Error("Invalid signature"); return JSON.parse(body);} app.post("/webhooks/fileverity", express.raw({ type: "application/json" }), (req, res) => { try { const event = verify(process.env.FILEVERITY_WEBHOOK_SECRET, req.headers, req.body.toString("utf8")); handle(event); res.sendStatus(200); } catch { res.sendStatus(400); }});import base64import binasciiimport hashlibimport hmacimport jsonimport osimport time from fastapi import FastAPI, HTTPException, Request app = FastAPI() def verify(secret, headers, body): message_id = headers["webhook-id"] timestamp = headers["webhook-timestamp"] if abs(time.time() - int(timestamp)) > 300: raise ValueError("Timestamp outside tolerance") key = base64.b64decode(secret.removeprefix("whsec_")) signed = (message_id + "." + timestamp + ".").encode() + body expected = hmac.new(key, signed, hashlib.sha256).digest() for entry in headers["webhook-signature"].split(" "): version, _, value = entry.partition(",") try: given = base64.b64decode(value, validate=True) except binascii.Error: continue if version == "v1" and hmac.compare_digest(given, expected): return json.loads(body) raise ValueError("Invalid signature") @app.post("/webhooks/fileverity")async def receive(request: Request): body = await request.body() try: event = verify(os.environ["FILEVERITY_WEBHOOK_SECRET"], request.headers, body) except (KeyError, ValueError): raise HTTPException(status_code=400) handle(event) return {"ok": True}Verifique con los bytes exactos que se enviaron. Analizar el JSON y volver a serializarlo los modifica e invalida la firma.
Envío y reintentos
- Responda con cualquier estado
2xxen un plazo de 10 segundos. No se siguen las redirecciones y se cuentan como fallos. - Cualquier otra respuesta, incluidos un tiempo de espera agotado o un error de conexión, programa un reintento con esperas crecientes: 5, 10, 20, 40, 80, 160, 320 y 640 minutos y, después, cada 12 horas. Los reintentos terminan 3 días después del evento. Los reintentos se ejecutan de forma programada, así que un reintento puede llegar más tarde que su espera, nunca antes.
- Los eventos se envían al menos una vez y no se garantiza el orden. Use
webhook-idpara ignorar duplicados y obtenga el análisis si necesita el estado más reciente. - La Consola muestra los últimos 50 envíos con su estado y sus errores, los conserva 30 días y permite reenviar cualquiera de ellos. Abra un envío para ver el contenido de la solicitud, el tiempo de respuesta y los primeros 2 KB de la respuesta de su endpoint, así que no devuelva secretos ni datos personales en esa respuesta. Un reenvío conserva el ID del evento original.
- Las solicitudes llevan el agente de usuario
Fileverity-Webhooks/1.0.
Desactivación automática
Un endpoint que falla de forma continua durante 3 días se desactiva. Entonces deja de recibir eventos nuevos y sus eventos no entregados se marcan como fallidos. Un envío correcto antes de ese plazo borra el fallo. Se avisa por correo a los propietarios y administradores del espacio de trabajo cuando se desactiva un endpoint. Vuelva a activar el endpoint en la Consola cuando lo haya corregido.
El evento de prueba
Use Enviar prueba en un endpoint de la Consola para enviar de inmediato un evento de ejemplo. Es un evento analysis.completed firmado, con data.test establecido en true y sin informe, de modo que pueda comprobar su URL y su código de verificación de firmas antes de que lleguen eventos reales.
{ "id": "evt_Qm4vB7nP1sK8tL3wR6yZ2cXa", "object": "event", "type": "analysis.completed", "created": 1793700500, "data": { "id": "an_Pw3mY6zB8Hk2pQ4vN9sL1xR7", "object": "analysis", "created": 1793700120, "model": "fvimg-2-2026-10-30", "livemode": true, "status": "succeeded", "file": { "name": "claim-photo.jpg", "bytes": 2481920, "format": "jpeg" }, "report_id": null, "report_url": null, "summary": null, "result": null, "error": null, "progress": null, "usage": { "amount": 199, "currency": "usd", "funding": "balance" }, "metadata": { "order_id": "A-10492" }, "completed_at": 1793700131, "expires_at": 1794304931, "test": true }}Relacionado: Análisis, Idempotencia y Errores.