Webhooks

What a webhook does

A webhook pushes results to you instead of making you ask for them. Set an HTTPS endpoint on a flow, and every time a run completes, Tavnit sends that run's extracted rows to your URL as a JSON POST, usually within seconds of the run finishing.

The alternative is polling: calling the API on a timer to ask whether anything finished. Polling costs you requests, adds latency, and gets worse as volume grows. A webhook arrives once, when there is something to deliver.

Use a webhook whenUse something else when
You want results in your own system the moment they existA person needs to read them: email output is better
You are wiring Tavnit into Make, Zapier, n8n or Power AutomateYou want the data queryable inside Tavnit: use a Bucket
Volume is high enough that polling is wastefulYou are fetching a specific known run: call the API directly

Which features can send webhooks

Flows are the most common source, but most features that produce a result can POST it to a URL. Each one is configured on its own detail page and sends its own payload.

SourceWhere you set itFires when
FlowWebhook panelA run completes successfully (after approval, if review is on)
CleanerWebhook panelA sweep the Cleaner runs on its own finishes (Clean Dataset and API sweeps). Flow runs deliver through the flow's webhook instead.
Cleaner conditional actionA webhook action in Conditional ActionsA rule matches: once per rule per run, not once per row
AgentDelivery, Webhook optionAn agent run completes
MatcherWebhook panelA match completes
InspectorWebhook panel, and Call webhook under If this check failsAn inspection finishes; the per-check action fires for each failed check
FillerWebhook panelA fill completes
SignalWebhook panelA Wave finishes
NetWebhook panelA Catch finishes with at least one row
PipelineAn Output node set to Webhook, Zapier, Make, n8n, Slack, Teams or Google ChatThe execution reaches the Output node
HTTPS only

Payloads contain your document data, so use an https:// endpoint. The Webhook panels refuse to save a plain http:// URL.

Set up a flow webhook

Flow webhooks are configured per flow. You need an HTTPS endpoint that accepts a POST with a JSON body.

  1. 1Get an endpoint URL. Automation platforms hand you one when you create a webhook trigger; otherwise expose your own HTTPS route.
  2. 2Open the flow in Flows.
  3. 3Open the Webhook panel, paste the URL and save.
  4. 4Process one test document and confirm the POST arrived. The run's log records whether delivery succeeded and what status code came back.
A Tavnit flow detail page for Invoice Processor, showing the left rail with Email Trigger, Collections, Cleaner, Agent, Form Templates, Email Output, Webhook, Bucket Export and Human in the Loop, next to the flow's metadata and table fields.
Webhook sits with the other output options in a flow's left rail, alongside Email Output and Bucket Export.

What a flow payload looks like

The body is the run's output plus its identifiers. Repeating line items arrive under rows, single-value fields under metadata, and run_id and flow_id tell you which run produced them.

JSON: flow webhook body
{
  "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
  }
}

The field names inside rows and metadata are the ones you defined on the flow, so the payload changes shape when you change the schema. If a Cleaner is attached, what you receive is the cleaned output: converted currencies, computed columns and all. If the Cleaner has a pivot applied to the webhook payload, the rows arrive in the pivoted, wide format.

KeyAlways presentWhat it is
run_idYesThe run that produced this result.
flow_idYesThe flow that processed the document.
rowsYesOne entry per extracted line item. An empty array is valid: some documents have no table.
metadataYesSingle-value fields that describe the document as a whole.
collection_run_idNoPresent when a Collection routed the document to this flow.
split_id, splitter_doc_titleNoPresent when a Splitter produced this segment.
JSON: provenance keys
{
  "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": {}
}
Files arrive as links, not bytes

Fields holding a file or an image are not embedded in the JSON. They come through as time-limited URLs, because stored documents are private: a raw storage path would not be fetchable from your server. Download them promptly rather than storing the link.

Payloads from other sources

Every source sends JSON, but the keys differ. Most carry the source's result plus the ids you need to look it up in Tavnit.

SourceKeys in the body
Cleaner sweepThe cleaned output, plus sweep_id, cleaner_id, and run_id when an API sweep re-cleans a run.
Cleaner conditional actioncontent (the message you composed, which can include the matching rows), receivers, run_id, flow_id, flow_name, sweep_id and matched_rows (how many rows matched).
Agentbot_id, bot_run_id, status, inputs, output, and flow_run_id when a flow run triggered the agent.
MatcherThe match result, plus match_id and matcher_id.
Inspectorinspection_id, inspector_id, verdict and the full report in output_json. A per-check webhook sends the failed check in item instead of the report.
Fillerfill_id, filler_id, status, filled_forms (plus filled_form_path for the first form) and the field values in output_json.
SignalThe Wave's result, plus wave_id and signal_id.
Netcolumns, rows, catch_id, net_id, window_start, window_end and a stats summary.
Pipeline Outputpipeline_id, execution_id, pipeline_name, original_filename and outputs, one entry per upstream node.
JSON: agent webhook body
{
  "bot_id": "...",
  "bot_run_id": "...",
  "status": "completed",
  "flow_run_id": "...",   // only when a flow run triggered the agent
  "inputs": { "supplier_name": "Acme Ltd" },
  "output": { ... }
}

An agent webhook always includes the agent's input variables (secrets are never included) so your receiver can join the captured output back onto its own records. The flow_run_id tells you which document the agent was working from when a flow run started it.

A conditional action is dispatched as soon as the rule matches, before any review pause. That is deliberate: alert me when this happens should not wait on a reviewer. The flow webhook, by contrast, only fires after a reviewer approves.

Pipeline Output to Slack, Teams and Google Chat

In a Pipeline, an Output node sends the results of the nodes feeding it to a destination you pick: Email, Slack, Teams, Google Chat, Webhook, Zapier, Make or n8n.

  • Slack, Teams and Google Chat receive a readable message with the key fields, posted through the incoming-webhook URL you paste from that app.
  • Webhook, Zapier, Make and n8n receive the raw JSON shown below.
  • If a flow in the pipeline already sends its own webhook, the pipeline warns you that an Output would post the same result twice.
JSON: pipeline Output body
{
  "pipeline_id": "...",
  "execution_id": "...",
  "pipeline_name": "Supplier intake",
  "original_filename": "bundle.pdf",
  "outputs": [
    { "node": "Invoice Processor", "node_type": "flow", "output": { ... } }
  ]
}

Delivery, timeouts and retries

Every source uses the same delivery rules. Tavnit waits up to 10 seconds for your endpoint to respond. A connection failure or timeout is retried once after a short pause; an HTTP error response is not retried, because your server was reached and answered.

What your endpoint doesWhat Tavnit does
Responds 2xx within 10 secondsDelivery is recorded as sent. Done.
Connection refused, dropped, or times outRetried once after a short pause. If the retry also fails, delivery is marked failed.
Responds 4xx or 5xxNot retried. The status code is recorded so you can see what your server said.

There is no long retry queue and no dead-letter replay. If your endpoint is down for an hour, those deliveries are lost: the runs still succeeded and their data is still in Tavnit, but you will have to fetch it over the API or re-deliver it another way. For anything you cannot afford to miss, pair the webhook with a Bucket so there is always a durable copy.

A failed webhook never fails the run

Delivery is best-effort and separate from processing. If your endpoint is unreachable, the run still completes, the data is still stored, and every other output (email, Bucket export, form fill) still fires. The exception is a pipeline Output node: a failed delivery marks that node as failed so you can see it on the execution.

Writing a receiver

The single most important rule: acknowledge fast, then work. Ten seconds sounds generous until your handler writes to a slow database. Return 200 as soon as you have the payload safely queued, and do the real processing afterwards.

Python (Flask)
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}), 200
JavaScript (Express)
import 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);
  • Accept a reasonably large body: a long invoice with many line items is not small.
  • Treat delivery as at-least-once. A retry after a timeout can deliver the same result twice, so make your handler idempotent by keying on run_id (or the source's own id).
  • Do not assume a fixed schema. Read fields by name and tolerate ones you do not recognise, so adding a flow field does not break your receiver.
  • Log the raw body on failure. It is the only copy of what arrived.
Keep the URL secret

The endpoint URL is the only thing standing between the internet and your extracted data. Automation platforms embed a secret token in the path for exactly this reason. Do not publish it, and rotate it if it leaks.

Troubleshooting

Start at the run, not at your server. Every run logs whether webhook delivery was attempted, whether it succeeded, and what status code or error came back, which immediately tells you whether the problem is Tavnit-side or yours.

SymptomLikely causeFix
Nothing arrives, no attempt loggedNo webhook URL is set on that flow, or the run failed before delivery.Check the flow's Webhook panel and the run's status.
The URL was rejected when savingIt does not start with https://.Use an HTTPS endpoint. Plain HTTP is not accepted.
The Cleaner webhook never fires for flow runsCleaner webhooks only fire for sweeps the Cleaner runs on its own.Use the flow's Webhook panel; it already carries the cleaned output.
Delivery logged as failed with a status codeYour endpoint returned 4xx or 5xx. It was reached, so there was no retry.Read your own server's logs: the payload is usually fine and the handler threw.
Delivery logged as failed with a timeoutYour handler took longer than 10 seconds.Acknowledge first and process asynchronously, as above.
The same run arrived twiceA retry followed a timeout on a request your server actually processed.De-duplicate on run_id.
Results arrive much later than expectedThe flow has human review enabled.The webhook fires on approval, not on extraction. That is by design.