Webhooks
Qué hace un webhook
Un webhook te envía los resultados en lugar de obligarte a pedirlos. Configura un endpoint HTTPS en un Flow y, cada vez que un Run termina, Tavnit envía las filas extraídas de ese Run a tu URL como un POST con JSON, normalmente pocos segundos después de que el Run termina.
La alternativa es el sondeo: llamar a la API cada cierto tiempo para preguntar si algo terminó. El sondeo consume solicitudes, agrega demora y empeora a medida que crece el volumen. Un webhook llega una sola vez, cuando hay algo que entregar.
| Usa un webhook cuando | Usa otra opción cuando |
|---|---|
| Quieres los resultados en tu propio sistema en cuanto existen | Una persona necesita leerlos: la salida por email es mejor |
| Estás conectando Tavnit con Make, Zapier, n8n o Power Automate | Quieres consultar los datos dentro de Tavnit: usa un Bucket |
| El volumen es tan alto que el sondeo resulta un desperdicio | Buscas un Run concreto que ya conoces: llama a la API directamente |
Qué funciones pueden enviar webhooks
Los Flows son el origen más común, pero la mayoría de las funciones que producen un resultado pueden enviarlo por POST a una URL. Cada una se configura en su propia página de detalle y envía su propio payload.
| Origen | Dónde se configura | Cuándo se dispara |
|---|---|---|
| Flow | Panel Webhook | Un Run termina con éxito (después de la aprobación, si la revisión está activa) |
| Cleaner | Panel Webhook | Termina una limpieza que el Cleaner ejecuta por su cuenta (Limpiar Conjunto de Datos y limpiezas por API). Los Runs de un Flow se entregan por el webhook del Flow. |
| Acción condicional de un Cleaner | Una acción de webhook en Acciones Condicionales | Se cumple una regla: una vez por regla por Run, no una vez por fila |
| Agente | Entrega, opción Webhook | Un Run del Agente termina |
| Matcher | Panel Webhook | Un Match termina |
| Inspector | Panel Webhook, y Llamar webhook en Si este check falla | Termina una Inspección; la acción por check se dispara por cada check que falla |
| Filler | Panel Webhook | Un Fill termina |
| Signal | Panel Webhook | Termina una Wave |
| Net | Panel Webhook | Termina un Catch con al menos una fila |
| Pipeline | Un nodo Salida configurado como Webhook, Zapier, Make, n8n, Slack, Teams o Google Chat | La ejecución llega al nodo Salida |
Los payloads contienen los datos de tus documentos, así que usa un endpoint https://. Los paneles Webhook no permiten guardar una URL http://.
Configura el webhook de un Flow
Los webhooks de Flow se configuran por Flow. Necesitas un endpoint HTTPS que acepte un POST con un cuerpo JSON.
- 1Consigue la URL de un endpoint. Las plataformas de automatización te dan una al crear un disparador de webhook; si no, expón tu propia ruta HTTPS.
- 2Abre el Flow en Flows.
- 3Abre el panel Webhook, pega la URL y guarda.
- 4Procesa un documento de prueba y confirma que llegó el POST. El registro del Run indica si la entrega tuvo éxito y qué código de estado se recibió.

Cómo es el payload de un Flow
El cuerpo es la salida del Run más sus identificadores. Las partidas repetidas llegan en rows, los campos de valor único en metadata, y run_id y flow_id te dicen qué Run los produjo.
{
"run_id": "8f1c2b7e-4d3a-4a91-9c11-2f7b6e0d5a44",
"flow_id": "3a9d51c0-77b2-4e18-9f6d-0c4a1b8e2d63",
"rows": [
{
"Description": "Software Platform Subscription — January",
"Quantity": 1,
"Price": 180.00,
"Amount": 180.00,
"Invoice Number": "001",
"Issued Date": "2026-01-15",
"Total": 270.30
}
],
"metadata": {
"Invoice Number": "001",
"Billed To": "Acme Ltd",
"Total": 270.30
}
}Los nombres de campo dentro de rows y metadata son los que definiste en el Flow, así que el payload cambia de forma cuando cambias el esquema. Si hay un Cleaner conectado, recibes la salida limpia: monedas convertidas, columnas calculadas y todo lo demás. Si el Cleaner tiene un pivote aplicado al payload del webhook, las filas llegan en el formato pivotado (ancho).
| Clave | Siempre presente | Qué es |
|---|---|---|
run_id | Sí | El Run que produjo este resultado. |
flow_id | Sí | El Flow que procesó el documento. |
rows | Sí | Una entrada por cada partida extraída. Un arreglo vacío es válido: algunos documentos no tienen tabla. |
metadata | Sí | Campos de valor único que describen el documento completo. |
collection_run_id | No | Aparece cuando una Colección enrutó el documento a este Flow. |
split_id, splitter_doc_title | No | Aparece cuando un Splitter produjo este segmento. |
{
"run_id": "...",
"flow_id": "...",
// present when a Collection routed the document
"collection_run_id": "b21e9f34-8c55-4d70-a6e2-91f0c7d43a18",
// present when a Splitter produced this segment
"split_id": "c74a0b12-3e69-4f85-b0d7-58e2a9c61f70",
"splitter_doc_title": "Commercial Invoice",
"rows": [],
"metadata": {}
}Los campos que contienen un archivo o una imagen no se incrustan en el JSON. Llegan como URLs temporales, porque los documentos almacenados son privados: una ruta de almacenamiento sin firmar no se podría descargar desde tu servidor. Descárgalos pronto en lugar de guardar el enlace.
Payloads de otros orígenes
Todos los orígenes envían JSON, pero las claves cambian. La mayoría incluye el resultado del origen más los identificadores que necesitas para encontrarlo en Tavnit.
| Origen | Claves en el cuerpo |
|---|---|
| Limpieza de un Cleaner | La salida limpia, más sweep_id, cleaner_id, y run_id cuando una limpieza por API vuelve a limpiar un Run. |
| Acción condicional de un Cleaner | content (el mensaje que redactaste, que puede incluir las filas que coinciden), receivers, run_id, flow_id, flow_name, sweep_id y matched_rows (cuántas filas coincidieron). |
| Agente | bot_id, bot_run_id, status, inputs, output, y flow_run_id cuando un Run de Flow disparó al Agente. |
| Matcher | El resultado del Match, más match_id y matcher_id. |
| Inspector | inspection_id, inspector_id, verdict y el informe completo en output_json. Un webhook por check envía el check fallido en item en lugar del informe. |
| Filler | fill_id, filler_id, status, filled_forms (más filled_form_path para el primer formulario) y los valores de los campos en output_json. |
| Signal | El resultado de la Wave, más wave_id y signal_id. |
| Net | columns, rows, catch_id, net_id, window_start, window_end y un resumen en stats. |
| Salida de un Pipeline | pipeline_id, execution_id, pipeline_name, original_filename y outputs, con una entrada por cada nodo anterior. |
{
"bot_id": "...",
"bot_run_id": "...",
"status": "completed",
"flow_run_id": "...", // solo cuando un Run de Flow disparó al Agente
"inputs": { "supplier_name": "Acme Ltd" },
"output": { ... }
}El webhook de un Agente siempre incluye sus variables de entrada (nunca los secretos) para que tu receptor pueda unir la salida capturada con sus propios registros. El flow_run_id te dice de qué documento partía el Agente cuando lo inició un Run de Flow.
Una acción condicional se envía en cuanto se cumple la regla, antes de cualquier pausa de revisión. Es intencional: avísame cuando pase esto no debería esperar a un revisor. El webhook del Flow, en cambio, solo se dispara después de que un revisor aprueba.
Salida de un Pipeline a Slack, Teams y Google Chat
En un Pipeline, un nodo Salida envía los resultados de los nodos que lo alimentan al destino que elijas: Email, Slack, Teams, Google Chat, Webhook, Zapier, Make o n8n.
- Slack, Teams y Google Chat reciben un mensaje legible con los campos clave, publicado mediante la URL de webhook entrante que copias de esa app.
- Webhook, Zapier, Make y n8n reciben el JSON sin procesar que se muestra abajo.
- Si un Flow del Pipeline ya envía su propio webhook, el Pipeline te avisa que una Salida publicaría el mismo resultado dos veces.
{
"pipeline_id": "...",
"execution_id": "...",
"pipeline_name": "Supplier intake",
"original_filename": "bundle.pdf",
"outputs": [
{ "node": "Invoice Processor", "node_type": "flow", "output": { ... } }
]
}Entrega, tiempos de espera y reintentos
Todos los orígenes siguen las mismas reglas de entrega. Tavnit espera hasta 10 segundos a que tu endpoint responda. Una falla de conexión o un tiempo agotado se reintenta una vez tras una pausa breve; una respuesta HTTP de error no se reintenta, porque tu servidor fue alcanzado y respondió.
| Qué hace tu endpoint | Qué hace Tavnit |
|---|---|
| Responde 2xx en menos de 10 segundos | La entrega queda registrada como enviada. Listo. |
| Rechaza la conexión, la corta o se agota el tiempo | Se reintenta una vez tras una pausa breve. Si el reintento también falla, la entrega queda marcada como fallida. |
| Responde 4xx o 5xx | No se reintenta. El código de estado queda registrado para que veas qué respondió tu servidor. |
No hay una cola de reintentos larga ni reenvío posterior de entregas fallidas. Si tu endpoint está caído durante una hora, esas entregas se pierden: los Runs igual tuvieron éxito y sus datos siguen en Tavnit, pero tendrás que obtenerlos por la API o reenviarlos de otra forma. Para lo que no te puedes permitir perder, combina el webhook con un Bucket para tener siempre una copia duradera.
La entrega es de mejor esfuerzo y está separada del procesamiento. Si tu endpoint no responde, el Run igual termina, los datos se guardan y todas las demás salidas (email, exportación a Bucket, llenado de formularios) se ejecutan. La excepción es el nodo Salida de un Pipeline: una entrega fallida marca ese nodo como fallido para que lo veas en la ejecución.
Cómo escribir un receptor
La regla más importante: confirma rápido y después trabaja. Diez segundos parecen suficientes hasta que tu manejador escribe en una base de datos lenta. Responde 200 en cuanto tengas el payload a salvo en una cola y haz el procesamiento real después.
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.post("/tavnit-webhook")
def receive():
payload = request.get_json(silent=True) or {}
run_id = payload.get("run_id")
rows = payload.get("rows", [])
# Acknowledge immediately. Tavnit waits 10 seconds for a response and
# treats a timeout as a failed delivery, so queue the slow work instead
# of doing it inline.
enqueue_processing(run_id, rows)
return jsonify({"received": True}), 200import express from "express";
const app = express();
app.use(express.json({ limit: "10mb" }));
app.post("/tavnit-webhook", (req, res) => {
const { run_id: runId, rows = [] } = req.body ?? {};
// Respond inside the 10-second window, then do the work.
res.status(200).json({ received: true });
enqueueProcessing(runId, rows).catch(console.error);
});
app.listen(3000);- Acepta cuerpos razonablemente grandes: una factura larga con muchas partidas no es pequeña.
- Trata la entrega como “al menos una vez”. Un reintento después de un tiempo agotado puede entregar el mismo resultado dos veces, así que haz tu manejador idempotente usando run_id (o el identificador propio del origen) como clave.
- No asumas un esquema fijo. Lee los campos por nombre y tolera los que no reconozcas, para que agregar un campo al Flow no rompa tu receptor.
- Registra el cuerpo sin procesar cuando algo falle. Es la única copia de lo que llegó.
La URL del endpoint es lo único que separa a internet de tus datos extraídos. Las plataformas de automatización incluyen un token secreto en la ruta justamente por eso. No la publiques y cámbiala si se filtra.
Solución de problemas
Empieza por el Run, no por tu servidor. Cada Run registra si se intentó la entrega del webhook, si tuvo éxito y qué código de estado o error se recibió, lo que te dice de inmediato si el problema está del lado de Tavnit o del tuyo.
| Síntoma | Causa probable | Solución |
|---|---|---|
| No llega nada y no hay intento registrado | El Flow no tiene URL de webhook, o el Run falló antes de la entrega. | Revisa el panel Webhook del Flow y el estado del Run. |
| La URL fue rechazada al guardar | No comienza con https://. | Usa un endpoint HTTPS. No se acepta HTTP sin cifrar. |
| El webhook del Cleaner nunca se dispara en los Runs de un Flow | Los webhooks de Cleaner solo se disparan en las limpiezas que el Cleaner ejecuta por su cuenta. | Usa el panel Webhook del Flow; ya incluye la salida limpia. |
| Entrega registrada como fallida con un código de estado | Tu endpoint respondió 4xx o 5xx. Fue alcanzado, así que no hubo reintento. | Revisa los registros de tu servidor: normalmente el payload está bien y el manejador lanzó un error. |
| Entrega registrada como fallida por tiempo agotado | Tu manejador tardó más de 10 segundos. | Confirma primero y procesa de forma asíncrona, como se explicó arriba. |
| El mismo Run llegó dos veces | Hubo un reintento después de un tiempo agotado en una solicitud que tu servidor sí procesó. | Elimina duplicados usando run_id. |
| Los resultados llegan mucho más tarde de lo esperado | El Flow tiene activada la Revisión Humana. | El webhook se dispara al aprobar, no al extraer. Es así por diseño. |
