Zum Hauptinhalt springen

Webhooks lokal entwickeln

Webhook-Endpunkte müssen öffentliche HTTPS-URLs sein, deshalb erreicht Formfeed den Server auf Ihrem Laptop nicht. formfeed listen schließt diese Lücke: Es leitet die Webhook-Ereignisse des Workspace an eine lokale URL weiter, signiert genau wie eine echte Zustellung. Der Handler, den Sie lokal schreiben, ist also der Handler, den Sie deployen. Kein Tunnel, kein temporärer Endpunkt, den Sie registrieren und später wieder löschen müssen.

$ formfeed listen --forward-to http://localhost:3000/webhooks
Ready. Forwarding test-environment events of workspace "Fennlor" to http://localhost:3000/webhooks
Webhook signing secret: whsec_3f9c… (kept for this API key across listen sessions)

12:04:11 render.completed rnd_8Hk2… → 200 (41 ms) evt_5b0c…
12:04:15 render.failed rnd_Q1a9… → 500 (12 ms) evt_91d2…
12:05:15 render.failed rnd_Q1a9… → 204 (9 ms) attempt 2 evt_91d2…

Sie brauchen die CLI (Node.js 22 oder neuer) und einen Schlüssel des Workspace mit dem Scope webhook:listen, den formfeed login Editoren, Admins und Inhabern gibt.

So funktioniert es​

Eine Listen-Sitzung ist ein Webhook-Endpunkt des Workspace, dessen Adresse ein Relay im Formfeed-Gateway ist. Ereignisse werden genau wie bei einem registrierten Endpunkt zugeordnet, signiert, wiederholt und protokolliert; nur der letzte Schritt ist anders.

  • Wann: im selben Moment wie für jeden anderen Endpunkt des Workspace. Ein lokaler Server sieht render.completed etwa ein bis zwei Sekunden nach dem Ende des Renderings.
  • Von wo: Die Anfrage an localhost kommt vom CLI-Prozess auf demselben Rechner. Nichts aus dem Internet verbindet sich mit Ihrem Laptop; die einzige Verbindung ist der WebSocket, den die CLI zum Gateway geöffnet hat. Das funktioniert hinter NAT, VPNs und Firmen-Proxys, die HTTPS zulassen.
  • Was ankommt: der bytegleiche Body und die Header Content-Type, User-Agent, Webhook-Id, Webhook-Timestamp, Webhook-Attempt und Webhook-Signature, wie unter Nutzlast beschrieben. Die Signatur verwendet das Geheimnis der Sitzung statt das eines Endpunkts.
  • Was zurückgeht: nur der Statuscode Ihres Servers. Der Render-Worker behandelt ihn wie die Antwort eines echten Endpunkts: 2xx gilt als zugestellt, alles andere wird nach 1 Minute, 5 Minuten, 30 Minuten, 2 Stunden und 12 Stunden wiederholt, und die Zustellung steht mit ihrem Status auf der Seite Webhooks der App. Ein lokaler Server, der nicht erreichbar ist oder nicht innerhalb von 8 Sekunden antwortet, wird als 502 protokolliert.
  • Wie lange eine Sitzung lebt: so lange, wie die CLI verbunden bleibt. Das Relay verlängert die Sitzung jede Minute, solange der WebSocket offen ist; stoppt die CLI oder bricht ihre Verbindung ab, endet die Sitzung.

Einen Empfänger aufsetzen​

Beginnen Sie mit einem Handler, der die Signatur prüft. Er liest das Geheimnis aus einer Umgebungsvariablen, sodass derselbe Code in Produktion mit dem Geheimnis des Endpunkts läuft.

server.ts
import express from 'express';
import { verifyWebhookSignature } from '@formfeed/sdk';

const app = express();

// express.text behält den Body als genau den String, der signiert wurde
app.post(
'/webhooks',
express.text({ type: 'application/json' }),
async (req, res) => {
const ok = await verifyWebhookSignature(
process.env.FORMFEED_WEBHOOK_SECRET!,
req.get('webhook-signature'),
req.body,
);
if (!ok) return res.status(400).send('invalid signature');

const event = JSON.parse(req.body);
console.log(event.type, event.data.id);
res.sendStatus(204);
},
);

app.listen(3000);

Dann in einem zweiten Terminal:

  1. Melden Sie sich einmal mit formfeed login an (oder setzen Sie FORMFEED_API_KEY auf einen Schlüssel mit dem Scope webhook:listen).
  2. Starten Sie die Sitzung: formfeed listen --forward-to http://localhost:3000/webhooks.
  3. Kopieren Sie das ausgegebene Geheimnis nach FORMFEED_WEBHOOK_SECRET und starten Sie Ihren Server neu. Das Geheimnis gehört zu Ihrem API-Schlüssel; das nächste formfeed listen mit demselben Schlüssel signiert also mit demselben Geheimnis, und Ihre .env bleibt gültig.
  4. Rendern Sie mit einem Test-Schlüssel oder rufen Sie formfeed trigger auf, und sehen Sie zu, wie die Zustellung ankommt.

Jede Zeile zeigt die Uhrzeit, das Ereignis, das Rendering oder den Job (- bei einem Ereignis ohne, etwa einem Kontingent-Ereignis), die Antwort Ihres Servers (→ failed: mit dem Grund, wenn er nicht erreichbar war oder nicht rechtzeitig antwortete), ab dem zweiten Versuch dessen Nummer und die Ereignis-ID. Strg+C beendet die Sitzung; ab dann hören die Zustellungen an sie sofort auf.

Bricht die Verbindung zum Gateway ab, verbindet sich die CLI nach einer Pause von 1 bis 30 Sekunden selbst wieder, mit einer neuen Sitzung, und meldet „Reconnected with a new session; the signing secret is unchanged.“ Bekommt sie nach mehreren Versuchen keine Verbindung zurück, bricht sie mit einem Fehler ab. Ereignisse, die der alten Sitzung zugeordnet wurden, während die Verbindung unterbrochen war, werden nicht weitergeleitet: Ihre IDs stehen im Zustellprotokoll der App, und formfeed webhooks resend sendet sie an die neue Sitzung. Eine in der App beendete Sitzung beendet den Befehl.

Das Geheimnis wird von der vorherigen Sitzung des Schlüssels übernommen, egal wie lange diese zurückliegt: Die jeweils letzte Sitzung jedes Schlüssels bleibt dafür gespeichert. Nur ein Schlüssel, der noch nie gelauscht hat, erhält ein neues Geheimnis, und die CLI gibt es aus.

Optionen​

OptionStandardWirkung
--forward-to <url>http://localhost:3000/webhooksDie lokale URL, die die Ereignisse empfängt, http:// oder https://
--events <types>jeder Typ außer den Speicher-EreignissenKommagetrennte Ereignistypen, zum Beispiel render.completed,job.failed. Der Standard umfasst die Render-, Job- und Kontingent-Ereignisse; die Opt-in-Ereignisse storage.delivered und storage.failed werden nur weitergeleitet, wenn sie genannt sind
--liveausAuch Ereignisse von Live-Renderings weiterleiten. Ohne die Option werden nur Renderings mit Test-Schlüsseln (ff_test_…) weitergeleitet. Braucht zusätzlich den Scope webhook:manage, sonst antwortet der Start der Sitzung mit 403 forbidden
--print-secretausStartet eine Sitzung, gibt nur ihr Signaturgeheimnis aus und beendet die Sitzung wieder, für .env-Dateien: echo "FORMFEED_WEBHOOK_SECRET=$(formfeed listen --print-secret)" >> .env
--skip-verifyausAkzeptiert ein selbstsigniertes Zertifikat, wenn die lokale URL https:// ist
--jsonausGlobales Flag: eine JSON-Zeile pro Zustellung (und eine, sobald die Sitzung bereit ist, mit ihrem secret) statt der Tabelle
--print-secret beendet eine laufende Sitzung

Eine neue Sitzung beendet die vorherige Sitzung desselben Schlüssels, und --print-secret startet eine. Führen Sie es vor formfeed listen aus oder in einem Terminal mit einem anderen Schlüssel, sonst stoppt das laufende formfeed listen innerhalb einer Minute.

quota.warning und quota.exceeded gehören zu keiner Umgebung und werden weitergeleitet, wann immer ihr Typ ausgewählt ist, was er standardmäßig ist. Ein Rendering mit eigener webhook_url sendet weiterhin an diese URL; die Sitzung erhält dasselbe Ereignis zusätzlich, signiert mit dem Sitzungsgeheimnis.

Alle Befehle und Flags stehen in der CLI-Referenz.

Ein Ereignis erneut senden​

Solange eine Sitzung läuft, können Sie jedes gespeicherte Ereignis erneut an sie senden, ob es zugestellt wurde oder nicht:

formfeed webhooks resend evt_5b0c8e6f2d4a4c3e9b1a7f6e5d4c3b2a

Die Ereignis-ID steht am Ende jeder Zeile, die formfeed listen ausgibt, im Header Webhook-Id einer Zustellung und im Zustellprotokoll der App. Ohne --to geht das Ereignis an die Sitzung des formfeed listen, das auf diesem Rechner mit demselben API-Schlüssel läuft. --to <id> nennt stattdessen eine Listen-Sitzung oder einen registrierten Endpunkt; ein Endpunkt braucht webhook:manage. Die erneute Zustellung ist eine neue, mit frischer Signatur und Webhook-Attempt: 1, und der Body ist das gespeicherte Ereignis unverändert, samt seiner download_url, die inzwischen abgelaufen sein kann.

Über die API ist das POST /webhooks/events/{id}/resend mit der endpoint_id einer Sitzung oder eines registrierten Endpunkts; die SDKs bieten client.webhooks.resend(eventId, endpointId) und client.webhooks.resend(event_id, endpoint_id). Die möglichen Antworten stehen unter Ein Ereignis erneut senden.

Ein echtes Ereignis auslösen​

formfeed trigger render.completed --template invoice --data templates/invoice/data/default.json

rendert die Vorlage wirklich und asynchron. Das Ereignis, das Ihr Handler erhält, ist also echt und kein Fixture: Das Rendering existiert, download_url funktioniert, und GET /renders/{id} liefert es. Das Rendering trägt "meta": { "source": "formfeed trigger" }. Ohne --data verwendet die CLI den Datensatz default der Vorlage im Projekt, sonst die Beispieldaten ihrer veröffentlichten Version (ihrer neuesten Version, wenn keine veröffentlicht ist). Passen die Daten nicht, lautet das Ereignis render.failed.

Verwenden Sie einen Test-Schlüssel: Test-Renderings kosten keine Einheiten, und ihre Ereignisse erreichen eine Sitzung ohne --live. Mit einem Live-Schlüssel warnt die CLI, dass das Rendering abgerechnet wird. render.completed ist das einzige Ereignis, das trigger erzeugt; jedes andere gespeicherte Ereignis senden Sie mit formfeed webhooks resend erneut.

Grenzen​

GrenzeWert
Sitzungen pro Workspace3 gleichzeitig; eine vierte antwortet mit 409 listen_session_limit
Sitzungen pro Schlüssel oder Mitglied1: Eine neue Sitzung beendet Ihre vorherige sofort, und das formfeed listen, das sie hielt, stoppt innerhalb einer Minute mit der Meldung, dass die Sitzung beendet wurde
Zustellungen pro Sitzung20 pro Sekunde; weitere werden mit 429 beantwortet und nach dem üblichen Zeitplan wiederholt
Zeit für Ihren lokalen Server8 Sekunden; danach zählt die Zustellung als fehlgeschlagen und wird wiederholt
KostenKeine; Sitzungen werden nicht abgerechnet

Ist die Sitzung beendet, hören Zustellungen an sie sofort und ohne Wiederholung auf, und ein erneutes Senden an sie antwortet mit 410 listen_session_ended.

Ein fehlerhafter lokaler Server deaktiviert nie etwas: Die Regel, die einen Endpunkt nach fünf erschöpften Ereignissen in Folge deaktiviert, gilt für Sitzungen nicht. Beendete Sitzungen und ihre Zustellungen werden nach einem Tag gelöscht, außer der letzten Sitzung jedes Schlüssels, die sein Signaturgeheimnis hält.

Sicherheit​

  • Wer lauschen darf: Schlüssel mit dem Scope webhook:listen. Neue API-Schlüssel haben ihn standardmäßig nicht; formfeed login gibt ihn Editoren, Admins und Inhabern. Live-Ereignisse brauchen zusätzlich webhook:manage, den formfeed login nur Admins und Inhabern gibt, damit ein Entwickler nicht versehentlich Dokumente von Kunden sieht.
  • Was eine Sitzung preisgibt: dieselben Ereignisse und signierten Download-Links, die Mitglieder schon im Zustellprotokoll der App sehen.
  • Durchgehend signiert: Das Relay prüft jede Zustellung mit dem Sitzungsgeheimnis, bevor es etwas weiterleitet, und Ihr Handler prüft sie noch einmal. Eine erratene Sitzungs-ID nützt nichts.
  • Nichts gespeichert: Das Relay hält nur den offenen WebSocket. Nutzlasten laufen durch Cloudflares EU-Jurisdiktion und werden dort nie gespeichert; das Zustellprotokoll in der Datenbank ist dasselbe wie bei jedem Endpunkt. Siehe Datenresidenz.
  • Kein Tunnel: listen leitet Formfeed-Ereignisse an eine lokale URL weiter. Es macht Ihren Server nicht erreichbar, nimmt keinen anderen Verkehr an und gibt nichts außer dem Statuscode zurück.
  • Sitzungen beenden: Die Seite Webhooks der App listet die laufenden Sitzungen unter CLI-Sessions; Inhaber und Admins beenden eine dort mit Session beenden. Über die API beendet DELETE /webhooks/listen/{id} eine Sitzung des Workspace, mit jedem Schlüssel, der webhook:listen hat.

Ohne die CLI​

formfeed listen ist ein Client von POST /webhooks/listen (Scope webhook:listen). Der Body nimmt events (eine Liste von Ereignistypen, mit demselben Standard wie die CLI) und live (true, um Live-Renderings einzuschließen, was webhook:manage braucht). Die Antwort ist 201 mit der id der Sitzung, ihrem secret, ihren events und environments, expires_at und einer websocket_url, die 60 Sekunden gilt und keinen Schlüssel braucht.

Über diesen WebSocket sendet das Relay JSON-Nachrichten: { "type": "ready", "session": "…" }, sobald die Verbindung steht, { "type": "delivery", "id": "…", "headers": { … }, "body": "…" } für jede Zustellung und { "type": "ended", "reason": "…" }, wenn die Sitzung anderswo beendet wurde. Beantworten Sie jede Zustellung innerhalb von 9 Sekunden mit { "type": "result", "id": "…", "status": 204 }, dem Status, den Ihr Server gegeben hat, oder 0, wenn er nicht erreichbar war. Das Relay schließt den Socket mit dem Code 4000, wenn die Sitzung beendet wurde, und 4001, wenn eine andere Verbindung sie übernommen hat. DELETE /webhooks/listen/{id} beendet die Sitzung. Die SDKs starten und beenden Sitzungen mit client.webhooks.listen.start() und .end(id); den WebSocket übernimmt Ihr Code.

In Produktion​

Registrieren Sie einen echten Endpunkt wie unter Webhooks beschrieben, legen Sie sein Geheimnis in FORMFEED_WEBHOOK_SECRET ab und deployen Sie denselben Handler. Mit environments des Endpunkts halten Sie Test-Ereignisse von Produktion fern.