Guide · Node.js (TypeScript)
PDF asynchron rendern und den Webhook prüfen mit Node.js (TypeScript)
Ein langer Bericht sollte keine HTTP-Anfrage offen halten. Im Async-Modus antwortet die API sofort mit einem eingereihten Rendering und ruft Ihren Endpunkt auf, wenn das PDF fertig ist. Dieser Guide enthält beide Hälften in Node.js (TypeScript): die Anfrage und einen Empfänger, der nur Zustellungen annimmt, die Formfeed wirklich signiert hat.
Kostenloser Plan mit 100 Einheiten im Monat, keine Kreditkarte
Was Sie brauchen
- Einen API-Key. Legen Sie ihn in der App unter API-Schlüssel an und exportieren Sie ihn als
FORMFEED_API_KEY. Zum Ausprobieren ist ein Test-Key (ff_test_…) richtig. Siehe Quickstart. - Node.js (TypeScript). Sie brauchen Node.js 22 oder neuer und das Paket
@formfeed/sdk, das selbst keine Abhängigkeiten hat. - Die Vorlage.
revenue-reportaus dem Berichtsbeispiel und dessendata.jsonneben dem Programm. - Ein Secret. 16 bis 200 zufällige Zeichen in
FORMFEED_WEBHOOK_SECRET, beiden Programmen bekannt. Es signiert die Zustellungen dieses Renderings, Retries eingeschlossen. - Eine öffentliche URL. Ersetzen Sie
https://example.com/hooks/formfeeddurch Ihren Endpunkt. Während der Entwicklung leitetformfeed listenZustellungen ohne Tunnel anlocalhostweiter.
npm install @formfeed/sdk
npm install --save-dev tsx # führt die .ts-Datei direkt ausDas Programm
Die Anfrage nennt die Vorlage, schaltet auf mode: "async" und sagt, wohin gemeldet wird. Die Antwort kommt sofort, mit dem Status queued.
import { readFile } from 'node:fs/promises';
import { Formfeed } from '@formfeed/sdk';
const formfeed = new Formfeed({ apiKey: process.env.FORMFEED_API_KEY! });
const data = JSON.parse(await readFile('data.json', 'utf8'));
// a problem answer (4xx, 5xx) throws a FormfeedError with the API's code, title and detail
const render = await formfeed.renders.create({
template: 'revenue-report',
data,
mode: 'async',
webhook_url: 'https://example.com/hooks/formfeed',
webhook_secret: process.env.FORMFEED_WEBHOOK_SECRET!,
});
console.log(render.id, render.status); // queued: the webhook reports the restAusführen
Es gibt die ID des Renderings und queued aus. Wenige Sekunden später erhält Ihr Endpunkt render.completed mit dem vollständigen Rendering samt download_url, oder render.failed mit dem Grund.
export FORMFEED_API_KEY=ff_test_...
export FORMFEED_WEBHOOK_SECRET=$(openssl rand -hex 24)
npx tsx render.tsWas an Node.js (TypeScript) besonders ist
- Top-Level-await. Die Datei verwendet
awaitaußerhalb einer Funktion, sie muss also ein ES-Modul sein:"type": "module"in derpackage.jsonoder die Endung.mts. - Retries sind eingebaut. Das SDK sendet mit jedem Rendering einen
Idempotency-Keyund wiederholt429,503und Netzwerkfehler bis zu dreimal mit Backoff, mit demselben Schlüssel: Ein Retry einer Anfrage, die schon beantwortet wurde, erhält diese Antwort zurück statt eines zweiten Renderings. - Typisierte Fehler. Eine Problem-Antwort wirft einen
FormfeedError, dessencodeder Fehlercode der API ist (template_not_found,quota_exceeded, …); fangen Sie ihn dort, wo Sie dem Nutzer etwas Sinnvolles zeigen können. - Läuft überall, wo es
fetchgibt. Das SDK nutzt nur Web-APIs, derselbe Aufruf funktioniert also in Deno, Bun und Cloudflare Workers; nur der Dateizugriff ist anders.
Der Empfänger
Er lauscht auf Port 8080, liest den rohen Body, prüft Signatur und Alter des Zeitstempels und sieht sich erst dann das JSON an. Eine Zustellung, die die Prüfung nicht besteht, erhält 401.
import { createServer } from 'node:http';
import { parseWebhookEvent } from '@formfeed/sdk';
const secret = process.env.FORMFEED_WEBHOOK_SECRET!;
createServer(async (request, response) => {
// the signature covers the bytes on the wire: read the raw body, never re-serialised JSON
const chunks: Buffer[] = [];
for await (const chunk of request) chunks.push(chunk as Buffer);
const body = Buffer.concat(chunks).toString('utf8');
try {
// checks the HMAC and the five-minute tolerance, throws when either fails
const event = await parseWebhookEvent(secret, request.headers['webhook-signature'] as string, body);
const render = event.data as { id: string; download_url: string | null };
if (event.type === 'render.completed') console.log('done', render.id, render.download_url);
if (event.type === 'render.failed') console.error('failed', render.id);
response.writeHead(204).end();
} catch {
response.writeHead(401).end();
}
}).listen(8080);Empfänger starten
FORMFEED_WEBHOOK_SECRET=… npx tsx hook.tsPrüfen in Node.js (TypeScript)
- Ein Aufruf prüft.
parseWebhookEventprüft HMAC und Fünf-Minuten-Toleranz mit Web Crypto und wirft, wenn eines von beiden nicht stimmt. - In Express. Hängen Sie
express.text({ type: "application/json" })an die Webhook-Route: Es behält den Body als den String, der signiert wurde, und den nimmtparseWebhookEvent. Mitexpress.json()ist der Body bereits geparst, und die Signatur lässt sich nicht mehr prüfen.
Was ankommt
Ein POST mit Webhook-Signature: t=<Unix-Sekunden>,v1=<hex>; das Hex ist HMAC-SHA256 über t, einen Punkt und den rohen Body, mit Ihrem Secret als Schlüssel, genau so, wie Sie es haben, als UTF-8-Bytes: das webhook_secret, das Sie gesendet haben, oder die ganze whsec_…-Zeichenkette eines registrierten Endpunkts samt Präfix. Jeder Versuch wird mit eigenem t neu signiert. Webhook-Id bleibt über Retries gleich und ist damit Ihr Schlüssel zum Deduplizieren. data ist das Rendering, wie GET /renders/{id} es liefert:
{
"id": "evt_5b0c8e6f2d4a4c3e9b1a7f6e5d4c3b2a",
"type": "render.completed",
"created_at": "2026-09-18T10:42:00Z",
"workspace_id": "3a6f…",
"data": {
"id": "rnd_7da09ab6cd9946b798c187c2a9350852",
"status": "succeeded",
"download_url": "https://cdn.formfeed.dev/o/eu/…/document.pdf?exp=…&sig=…",
"page_count": 2,
"units": 1,
…
}
}Retries, und was Ihr Endpunkt leisten muss
- Schnell antworten. Eine Zustellung gelingt mit einem
2xxinnerhalb von 10 Sekunden. Speichern Sie das Event und erledigen Sie die Arbeit danach. - Retries. Alles andere, auch eine Weiterleitung, wird nach 1 Minute, 5 Minuten, 30 Minuten, 2 Stunden und 12 Stunden wiederholt: insgesamt sechs Versuche über etwa 14 Stunden und 40 Minuten. Dasselbe Event kann also zweimal ankommen: Deduplizieren Sie über
Webhook-Id. - Ohne Secret. Eine
webhook_urlohnewebhook_secretwird unsigniert zugestellt. Für signierte Zustellungen aller Renderings registrieren Sie einmal einen Endpunkt in der App oder überPOST /webhooks; er erhält ein eigenes Secret. - Lokal ausprobieren.
formfeed listen --forward-to http://localhost:8080/hooks/formfeedleitet die Events Ihrer Test-Renderings an Ihren Rechner weiter, signiert mit dem eigenen Secret der Session statt mitwebhook_secret: Starten Sie den Empfänger mit diesem, dasformfeed listen --print-secretausgibt (vor der Session aufrufen, denn es beendet eine laufende).formfeed trigger render.completed --template revenue-reporterzeugt ein echtes Test-Rendering, dessen Event Sie ankommen sehen. Der Schlüssel braucht den Scopewebhook:listen. Siehe Webhooks lokal entwickeln.
Wenn die API nein sagt
- Eine Form für jeden Fehler. Fehler sind Problem-Dokumente (RFC 9457) mit
type,title,status,detail, einem stabilencodeund derrequest_id, die Sie dem Support nennen; dietype-URL führt zu einer Seite über diesen Fehler. Die Liste steht in der Fehlerreferenz. - Die üblichen.
401 unauthorizedbei fehlendem oder widerrufenem Schlüssel,404 template_not_foundbei falschem Vorlagennamen,422 template_runtime_error, wenn die Vorlage an den Daten scheitert,429 rate_limitedüber den Anfragen pro Sekunde des Tarifs,402 quota_exceeded, wenn die Einheiten des Free-Tarifs verbraucht sind (ein bezahlter Tarif rechnet weitere Einheiten ab, bis zu seiner Obergrenze). - Sicher wiederholen. Senden Sie beim POST einen
Idempotency-Key-Header und wiederholen Sie die Anfrage mit demselben Schlüssel, wenn Sie nicht wissen, ob der erste Versuch ankam: 24 Stunden lang liefert der Schlüssel die erste Antwort, statt erneut zu rendern und zu berechnen. Gespeichert wird die Antwort, sobald der erste Versuch beantwortet ist; wiederholen Sie also nicht, solange er noch laufen kann. Auch Probleme unter 500 werden gespeichert, eine korrigierte Anfrage braucht deshalb einen neuen Schlüssel.
Die Aufgabe um dieses Programm
Dieser Guide ist ein Programm. Was die Aufgabe darum herum braucht, von der Vorlage bis zu Limits und Preis, steht auf der Seite Report-PDFs erzeugen.
Weitere Guides für Node.js (TypeScript)
- PDF-Rechnung aus JSON erzeugen mit Node.js (TypeScript)
- ZUGFeRD-E-Rechnung erstellen mit Node.js (TypeScript)
- HTML in PDF umwandeln mit Node.js (TypeScript)
- Open-Graph-Bilder erzeugen mit Node.js (TypeScript)
- PDFs aus einer CSV erzeugen mit Node.js (TypeScript)
- PDFs zusammenfügen und mit Passwort schützen mit Node.js (TypeScript)
FAQ
- Kann ich pollen, statt einen Webhook zu empfangen?
- Ja:
GET /renders/{id}, bis der Statussucceededoderfailedist; die SDKs haben dafürwaitForundwait_for. Ein Webhook spart die Anfragen und meldet sich früher. Siehe Async und Batch. - Warum muss der rohe Body verwendet werden?
- Die Signatur läuft über die Bytes auf der Leitung. JSON zu parsen und neu zu serialisieren kann Schlüssel umsortieren, Zahlenformate ändern oder Zeichen anders maskieren, und der HMAC dieses Textes passt dann nicht mehr.
- Wozu dient die Fünf-Minuten-Toleranz?
- Sie verhindert, dass eine abgefangene Zustellung später erneut eingespielt wird. Sie bedeutet auch, dass die Uhr Ihres Servers ungefähr stimmen muss.