Integration

Webhooks: Extrahierte Daten automatisch an dein System senden

Webhooks in Tiro einrichten: Ziel anlegen, Secret sichern, Payload und Signaturprüfung in Python oder Node – jedes fertige Dokument landet in deinem System.

Von Christian Gerloff · Veröffentlicht am 2. September 2026

Kurz gesagt: Ein Ziel je Inbox, eine URL, ein Secret. Sobald Tiro ein Dokument fertig verarbeitet hat, schickt es einen POST mit allen Feldern und Positionen als JSON an deine Adresse – signiert, mit Wiederholungen bei Störungen, und jeder Versuch ist im Verlauf des Dokuments sichtbar. Dein System muss nur eine URL anbieten und mit 2xx antworten.

Wofür Webhooks gedacht sind

Die Tabelle im Browser und der Excel-Download reichen, solange ein Mensch die Daten weiterträgt. Sobald sie ohne Umweg in ein anderes System sollen – ins ERP, in eine Datenbank, in ein Google Sheet über eine Automatisierungsplattform –, ist der Webhook der kürzeste Weg: Tiro meldet sich, dein System nimmt entgegen, fertig. Typische Empfänger sind ein kleiner Endpunkt in deiner eigenen Anwendung, ein Zapier-, Make- oder n8n-Webhook oder eine Serverless-Funktion.

Ein Ziel gehört zu genau einer Inbox. Du kannst mehrere Ziele je Inbox anlegen (etwa Test- und Produktivsystem), jedes einzeln aktivieren und deaktivieren, und jedes hat sein eigenes Secret.

Der Aufruf

Für jedes fertige Dokument sendet Tiro genau einen POST an die URL des Ziels, Content-Type: application/json, mit diesen Headern:

HeaderInhalt
X-Tiro-EventEreignisname, heute extract.completed (Test-Aufrufe: test)
X-Tiro-TimestampZeitpunkt des Aufrufs in Unix-Sekunden
X-Tiro-Signaturev1= gefolgt vom HMAC-SHA256 (hex) über <timestamp>.<body> mit deinem Secret
Idempotency-KeyID der Zustellung – bei jedem Wiederholungsversuch identisch
User-AgentTiro-Webhooks/1.0

Der Body ist immer derselbe Umschlag; die Werte unter extract.data folgen dem Datenvertrag deiner Inbox: ein Wert je Feld-Key, das Tabellen-Feld als Array von Zeilen, Datum als ISO 8601, Zahlen als Zahlen.

{
  "event": "extract.completed",
  "id": "9c1f4e0a-2b7d-4c0e-9a8b-5f6d7e8f9a0b",
  "created_at": "2026-09-02T09:41:12Z",
  "inbox": { "id": "3b2e…", "name": "Auftragsbestätigungen" },
  "document": {
    "id": "7d4a…",
    "filename": "AB-2026-0815.pdf",
    "kind": "pdf",
    "content_type": "application/pdf",
    "pages": 2,
    "credits": 2,
    "source": "email",
    "status": "done",
    "created_at": "2026-09-02T09:40:58Z"
  },
  "extract": {
    "data": {
      "order_number": "AB-2026-0815",
      "order_date": "2026-09-01",
      "supplier": "Example Supplies Ltd.",
      "total": 1234.5,
      "line_items": [
        { "description": "Steel bracket, 40 mm", "quantity": 12, "unit_price": 8.9, "amount": 106.8 },
        { "description": "Assembly", "quantity": 1, "unit_price": 1127.7, "amount": 1127.7 }
      ]
    },
    "created_at": "2026-09-02T09:41:11Z"
  },
  "links": {
    "document": "/api/v1/inboxes/3b2e…/documents/7d4a…",
    "extract": "/api/v1/inboxes/3b2e…/documents/7d4a…/extract",
    "download_csv": "/api/v1/inboxes/3b2e…/documents/7d4a…/download?format=csv",
    "download_xlsx": "/api/v1/inboxes/3b2e…/documents/7d4a…/download?format=xlsx"
  }
}

Die Datei selbst ist nie Teil des Aufrufs. Brauchst du sie, holst du sie über die Links mit deinem API-Schlüssel – so bleibt der Webhook klein und der Zugriff auf Dokumente bleibt bei deinem Schlüssel.

Signatur prüfen

Wer deine URL kennt, kann sie aufrufen. Die Signatur stellt sicher, dass der Aufruf wirklich von Tiro kommt und der Inhalt unverändert ist. Prüfe sie so: Nimm den rohen Body (Bytes, nicht das geparste JSON), setze <timestamp>.<body> zusammen, bilde den HMAC-SHA256 mit deinem Secret und vergleiche ihn in konstanter Zeit mit dem Wert hinter v1=. Verwirf Aufrufe, deren Zeitstempel mehr als fünf Minuten von deiner Uhr abweicht – das entschärft Wiederholungsangriffe.

Python (Flask):

import hmac, hashlib, time
from flask import Flask, request, abort

SECRET = "whsec_…"  # aus der App, einmalig angezeigt
app = Flask(__name__)

@app.post("/webhooks/tiro")
def tiro_webhook():
    timestamp = request.headers.get("X-Tiro-Timestamp", "")
    signature = request.headers.get("X-Tiro-Signature", "")
    body = request.get_data()  # rohe Bytes

    if abs(time.time() - int(timestamp or 0)) > 300:
        abort(400)
    expected = hmac.new(SECRET.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(f"v1={expected}", signature):
        abort(401)

    event = request.get_json()
    if event["event"] == "extract.completed":
        data = event["extract"]["data"]
        # … in ERP, Datenbank oder Tabelle schreiben
    return "", 204

Node (Express):

import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const SECRET = process.env.TIRO_WEBHOOK_SECRET; // whsec_…
const app = express();

app.post("/webhooks/tiro", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.get("X-Tiro-Timestamp") ?? "";
  const signature = req.get("X-Tiro-Signature") ?? "";
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(400);

  const expected = "v1=" + createHmac("sha256", SECRET).update(`${timestamp}.`).update(req.body).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(signature);
  if (a.length !== b.length || !timingSafeEqual(a, b)) return res.sendStatus(401);

  const event = JSON.parse(req.body);
  if (event.event === "extract.completed") {
    const data = event.extract.data;
    // … weiterverarbeiten
  }
  res.sendStatus(204);
});

Wichtig in beiden Fällen: Der Body muss roh geprüft werden, bevor ein Framework ihn parst und neu serialisiert – sonst stimmt die Signatur nie.

Wiederholungen und Idempotenz

Antworte schnell mit einem 2xx-Status und erledige langsame Arbeit danach; Tiro wartet höchstens zehn Sekunden. Bekommt Tiro 5xx, 408, 429 oder keine Antwort, wiederholt es den Aufruf nach 1, 5 und 15 Minuten, dann nach 1, 3, 6 und 12 Stunden – acht Versuche über rund einen Tag. Weil dein Server zwischendurch einen Aufruf verarbeitet haben kann, ohne dass die Antwort ankam, schickt Tiro bei jeder Wiederholung denselben Idempotency-Key. Merke dir verarbeitete Keys und überspringe Dubletten.

Antworten wie 400, 401, 404 oder Weiterleitungen (3xx) gelten als Ablehnung: Tiro wiederholt nicht, sondern markiert die Zustellung im Verlauf des Dokuments als fehlgeschlagen. Nach einer Korrektur an deinem Empfänger stößt du sie dort mit einem Klick erneut an. Wird ein Dokument neu verarbeitet, bekommt dein Ziel das neue Ergebnis als eigene Zustellung.

Sicherheit

  • Nur https, nur öffentlich: Tiro löst den Hostnamen vor jedem Aufruf auf und weist private, lokale und Metadaten-Adressen ab. Verbunden wird mit der geprüften Adresse; der Hostname dient nur noch der Zertifikatsprüfung.
  • Secret nur einmal sichtbar: Es wird verschlüsselt gespeichert und nie erneut angezeigt. „Secret rotieren“ erzeugt ein neues; das alte gilt sofort nicht mehr.
  • Keine Datei im Aufruf: Der Payload enthält Metadaten und Werte, nie den Dokumentinhalt. Downloads laufen über die API mit deinem Schlüssel.
  • Keine Weiterleitungen: Ein 3xx wird nicht gefolgt – deine URL muss die endgültige sein.

Über die API

Alles, was der Reiter „Ziele“ kann, geht auch per REST: POST /api/v1/destinations legt ein Ziel an (Antwort enthält das Secret einmalig), PATCH ändert URL oder Aktiv-Status, POST …/rotate-secret und POST …/test entsprechen den Schaltflächen. Wer lieber abfragt als angerufen zu werden, nutzt GET /api/v1/extracts?since=<ISO-Zeit> – neueste Ergebnisse zuerst, mit next_cursor zum Weiterblättern. Die Details stehen in der API-Dokumentation in der App.

So funktioniert es

  1. Ziel anlegen

    Öffne in der App die Inbox, wechsle in den Reiter „Ziele“ und trage die https-URL deines Empfängers ein. Tiro prüft die Adresse und zeigt dir das Signatur-Secret genau einmal an – kopiere es sofort in die Konfiguration deines Systems.

  2. Test-Ereignis senden

    Klicke auf „Test-Ereignis senden“. Tiro schickt einen Aufruf mit Beispieldaten an deine URL und zeigt HTTP-Code, Dauer und gegebenenfalls den Fehlertext an. Antwortet dein Server mit 2xx, ist alles bereit.

  3. Dokument verarbeiten und Zustellung prüfen

    Lade ein Dokument hoch oder leite es per E-Mail weiter. Sobald das Ergebnis fertig ist, ruft Tiro deine URL auf; im Verlauf des Dokuments siehst du jeden Zustellversuch mit Status und HTTP-Code – und kannst eine fehlgeschlagene Zustellung per Klick wiederholen.

Häufige Fragen

Was ist ein Webhook, kurz erklärt?

Ein Webhook ist ein HTTP-Aufruf, den Tiro an eine Adresse deiner Wahl schickt, sobald etwas passiert ist – hier immer dann, wenn ein Dokument fertig verarbeitet wurde. Statt regelmäßig nachzufragen, bekommt dein System die Daten geliefert.

Was kostet ein Webhook?

Nichts. Zustellungen verbrauchen keine Credits – gezählt wird nur die Verarbeitung des Dokuments, wie überall in Tiro.

Wie oft versucht Tiro es, wenn mein Server nicht antwortet?

Antwortet dein Server mit 5xx, 408 oder 429 oder gar nicht, wiederholt Tiro den Aufruf mit wachsenden Abständen – nach 1, 5 und 15 Minuten, dann nach 1, 3, 6 und 12 Stunden, insgesamt acht Versuche über rund einen Tag. Danach steht die Zustellung im Verlauf des Dokuments als fehlgeschlagen und du kannst sie von Hand erneut anstoßen. Andere Antworten wie 400 oder 404 gelten sofort als Ablehnung.

Warum lehnt Tiro meine URL ab?

Erlaubt sind nur https-Adressen zu öffentlich erreichbaren Servern. Adressen im privaten Netz, auf localhost oder bei Cloud-Metadaten-Diensten weist Tiro ab, ebenso URLs mit eingebetteten Zugangsdaten. Für lokale Tests eignen sich Tunnel-Dienste wie ngrok oder Cloudflare Tunnel.

Was passiert, wenn ich das Secret verliere?

Tiro speichert das Secret verschlüsselt und kann es nicht erneut anzeigen. Klicke bei dem Ziel auf „Secret rotieren“ – du bekommst ein neues Secret, das alte gilt sofort nicht mehr, und trägst es in deinem System ein.

Erfahre ich auch von Dokumenten, die nicht verarbeitet werden konnten?

Ja. Ein Ziel kann statt fertiger Ergebnisse die endgültig fehlgeschlagenen Dokumente abonnieren – beim Anlegen über die REST-API mit event document.failed. Der Aufruf trägt dann Status, Fehlermeldung und Links zum Dokument, aber kein Extrakt. In der App legst du weiterhin Ziele für Ergebnisse an.

Gibt es Zapier, Make oder n8n?

Noch nicht als fertige Apps. Beide Plattformen können aber heute schon mit Tiro arbeiten – über „Webhooks by Zapier“ bzw. den HTTP-Trigger in Make und n8n, die einen Webhook entgegennehmen, oder per Polling auf den REST-Endpunkt GET /extracts?since=. Fertige Apps in den Verzeichnissen sind geplant.