formbasedocs
Zur AppApp

Anfragen

Callbacks & Signierung

Wenn ein Request sein Ende erreicht — abgeschlossen, abgelaufen oder storniert — sendet formbase eine signierte Benachrichtigung per POST an die URL, die deine Automation angegeben hat. Dieser Aufruf setzt den Lauf fort.


Was feuert, und wann

EreignisWann
request.completedDie empfangende Person hat abgesendet. Trägt die Antworten.
request.expiredDer Ablauf ist eingetreten, während der Request noch ausstehend war.
request.canceledDu oder deine Automation habt ihn zurückgezogen.

Alle drei kommen an derselben URL an, also verzweige nach type, bevor du Antworten voraussetzt. Das ist der ganze Sinn davon, bei jedem Ende zu feuern: Ein bei einer Kundin geparkter Workflow wird fortgesetzt, egal ob sie geantwortet, dich ignoriert oder du den Request abgebrochen hast.

Was ankommt

POST body
json
{
  "id": "evt_kj7...",
  "type": "request.completed",
  "createdAt": "2026-03-04T09:31:40.000Z",
  "apiVersion": "2026-09-24",
  "test": false,
  "data": {
    "request": {
      "id": "kd7...",
      "externalId": "run-42",
      "status": "completed",
      "outcome": "approve",
      "language": "en",
      "recipient": { "email": "ada@acme.com", "name": "Ada" },
      "metadata": { "runId": "run-42" },
      "context": { "case_id": "CASE-9" },
      "createdAt": "2026-03-04T09:20:00.000Z",
      "completedAt": "2026-03-04T09:31:40.000Z"
    },
    "form": { "id": "j57...", "name": "Vendor onboarding", "snapshotId": "kx2..." },
    "submission": {
      "id": "jd7...",
      "respondentEmail": "ada@acme.com",
      "submittedAt": "2026-03-04T09:31:40.000Z",
      "updatedAt": null,
      "editCount": 0,
      "pdfUrl": null,
      "language": "en"
    },
    "answers": { "company_name": "Acme", "contacts": [{ "name": "Ada" }] },
    "display": { "company_name": "Acme", "contacts": "Ada" }
  }
}
  • data.request ist immer da — einschließlich deiner externalId, metadata und context, unverändert. Es trägt den Zeitstempel des jeweiligen Endes (completedAt, expiredAt oder canceledAt mit optionalem cancelReason).

  • outcome ist nur vorhanden, wenn die empfangende Person eine Entscheidungsfrage beantwortet hat — approve, decline oder changes.

  • form, submission, answers und display erscheinen nur beim Abschluss, genau in der Form, die auch ein Submission-Webhook trägt. form.snapshotId ist die genaue veröffentlichte Version, die die empfangende Person beantwortet hat; submission.pdfUrl ist nur dann eine URL, wenn das Formular ein Einreichungs-PDF aufbewahrt, sonst null.

  • test ist true, wenn der Request im Testmodus erstellt wurde — verzweige danach, oder verwirf das Ereignis.

  • answers ist nach Feldschlüssel keyed, wobei Wiederholungsgruppen als ein Objekt pro Instanz verschachtelt sind. Eine Auswahlantwort ist der Options-key aus fields.list, nicht sein Label; das Label steht in display, unter demselben Schlüssel.

  • Der POST kommt mit Content-Type: application/json und User-Agent: formbase an und trägt X-formbase-Event-Id, X-formbase-Event-Type und X-formbase-Signature — sodass du deduplizieren und routen kannst, bevor du parst.

Die Signatur prüfen

Jeder Callback trägt einen Signatur-Header, X-formbase-Signature: t={unix seconds},sha256={hex}. Der Hex-Wert ist ein HMAC-SHA256 aus dem Zeitstempel, einem Punkt und dem rohen Request-Body, berechnet mit dem Request-Signaturgeheimnis deines Workspace.

Zwei Regeln, egal welche Sprache du verwendest:

  1. Hashe den rohen Body, vor jedem Parsen oder erneuten Serialisieren. Neu kodiertes JSON sind nicht dieselben Bytes.

  2. Vergleiche in konstanter Zeit — crypto.timingSafeEqual, hmac.compare_digest — niemals mit ==.

Das Request-Signaturgeheimnis

Ein Geheimnis pro Workspace signiert jeden Callback aus ihm. Du findest es unter OAuth und API-Schlüssel in der Workspace-Seitenleiste, in der Karte Request-Signaturgeheimnis. Es ist standardmäßig maskiert; der Augen-Button zeigt es an, der Kopieren-Button kopiert es. Es ist kein Einmalwert — du kannst jederzeit zurückkehren und es erneut auslesen. Das Geheimnis wird beim ersten Mal erzeugt, an dem es gebraucht wird — ein Workspace, der diese Karte nie geöffnet und nie einen Request mit callbackUrl erstellt hat, hat also noch keines.

Wiederholungen

Ein Callback bekommt acht Versuche: den ersten, dann sieben Wiederholungen mit mindestens 1, 2, 4, 8, 16, 32 und 60 Minuten Abstand. formbase sucht alle 30 Minuten nach fälligen Wiederholungen, sodass eine Wiederholung bis zu einer halben Stunde nach Ablauf ihres Abstands ankommen kann und der letzte Versuch etwa vier Stunden nach dem Ende des Requests erfolgt. Jeder Versuch trägt dieselben Bytes und dieselbe id: Das Payload ist in dem Moment eingefroren, in dem der Request endete, sodass ein Retry beschreibt, was damals passiert ist, nicht wie der Request jetzt aussieht. Die Ziel-URL und das Signaturgeheimnis werden bei jedem Versuch neu gelesen, nicht mit eingefroren.

Deine AntwortWas formbase tut
2xxFertig. Der Callback wird als zugestellt markiert.
408, 429, 5xxWiederholt, unter Beachtung von Retry-After, falls du eines sendest.
Andere 4xxStoppt. Dein Endpunkt hat den Aufruf abgelehnt; denselben Body erneut zu senden kann nicht helfen.
Timeout oder VerbindungsfehlerWiederholt nach demselben Zeitplan.
Blockierte URLStoppt sofort. Ein Host, der sich nicht auflösen lässt, eine private Adresse oder eine Nicht-HTTPS-URL können nicht erlaubt werden. Weiterleitungen werden nie verfolgt, daher stoppt auch ein 3xx.

Ist das Budget aufgebraucht — dein Empfänger war den Nachmittag über offline — ist der Callback nicht verloren. Der Request bekommt ein Callback fehlgeschlagen-Badge, dem Workspace-Owner wird einmalig eine E-Mail mit Host, Grund und Versuchszahl geschickt, und die Antworten bleiben über requests.get lesbar. Um ihn erneut anzustoßen, öffne den Request auf der Requests-Seite und drücke Replay, oder rufe requests.replayCallback auf. Es sendet dasselbe eingefrorene Payload mit derselben id erneut — genau das, was ein deduplizierender Empfänger braucht.

Abonnements hören dieselben Ereignisse

Eine Callback-URL gehört zu einem Request. Wenn jeder Request auf einem Formular denselben Empfänger erreichen soll, abonniere stattdessen einmal: Die formbase-Apps für Zapier und n8n erledigen das für dich, und webhooks.create macht es per Code mit request_completed, request_expired oder request_canceled als Ereignistyp. Ein Abonnement empfängt denselben Umschlag, signiert mit seinem eigenen Geheimnis statt mit dem Request-Signaturgeheimnis des Workspace, mit eigener Event-ID und eigenem Wiederholungsbudget. Ein Request mit sowohl einer Callback-URL als auch einem passenden Abonnement feuert zweimal, einmal an jeden. Replay sendet nur den Callback erneut; ein Abonnement wiederholt eigenständig und pausiert nach fünf fehlgeschlagenen Versuchen.

Eine Resume-URL ist keine Authentifizierung

Workflow-Tools geben dir eine schwer zu erratende Resume-URL, und es ist verlockend, das als Beweis zu behandeln. Sie ist ein Bearer-Geheimnis — sie kann in Logs landen, und sie sagt dir nicht, dass der Body nicht manipuliert wurde. Verifiziere die Signatur auch im fortgesetzten Zweig.