> ## Documentation Index
> Fetch the complete documentation index at: https://formpaste.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Erhalte einen signierten HTTP-POST für jede zugestellte Einsendung - Pro.

<Info>**Pro** - das Konfigurieren und Empfangen von Webhook-Zustellungen erfordert Pro.</Info>

Formpaste kann jede Nicht-Spam-Einsendung in Echtzeit per `POST` an eine URL deiner Wahl senden,
signiert, damit du überprüfen kannst, dass sie tatsächlich von Formpaste stammt.

## Einen Webhook aktivieren

1. Öffne ein Formular im [Dashboard](https://app.formpaste.com) → **Einstellungen** →
   **Webhooks**.
2. Gib die `https://`-URL deines Endpunkts (`webhook_url`) ein und speichere.
3. Das Dashboard zeigt ein **Signaturgeheimnis** (`whsec_…`) für dieses Formular, kopiere es;
   du brauchst es, um eingehende Anfragen zu verifizieren.

## Wann er ausgelöst wird

Ein Webhook wird für jede **zugestellte, nicht als Spam eingestufte** Einsendung ausgelöst,
dieselben Einsendungen, die eine Benachrichtigungs-E-Mail erzeugen. Unter Quarantäne stehende
(Spam-)Einsendungen lösen keinen Webhook aus. Die Zustellung erfolgt nach bestem Bemühen und
unabhängig von der E-Mail: Ein Webhook-Fehler blockiert oder beeinträchtigt niemals deine
E-Mail-Benachrichtigung.

## Anfrage

`POST` an deine konfigurierte URL, `Content-Type: application/json`.

| Header                  | Wert                                                            |
| ----------------------- | --------------------------------------------------------------- |
| `X-Formpaste-Event`     | `submission.received`                                           |
| `X-Formpaste-Timestamp` | Unix-Epoch in **Millisekunden**, als die Anfrage signiert wurde |
| `X-Formpaste-Signature` | `sha256=<hex>`, HMAC-SHA256 (siehe Verifizieren unten)          |
| `User-Agent`            | `Formpaste-Webhook/1`                                           |

## Payload

Der Body ist ein versioniertes JSON-Envelope. Deine eingesendeten Felder liegen unter `data`:

```json theme={null}
{
  "type": "submission.received",
  "version": 1,
  "id": "sub_...",
  "form_id": "frm_...",
  "created_at": 1720700000000,
  "data": {
    "name": "John Smith",
    "email": "john@example.com",
    "message": "Hello!"
  }
}
```

* `type` / `version`: das Envelope. Heute existiert nur `submission.received` (Version `1`);
  neue Event-Typen können später hinzukommen.
* `data`: deine eingesendeten Felder, bereits bereinigt: `access_key`, das `botcheck`-Honeypot
  und jedes `redirect`-Steuerfeld werden entfernt und nie einbezogen.

<Note>
  Felder sind unter `data` verschachtelt, nicht auf der obersten Ebene abgeflacht, in Zapier,
  Make, Pipedream oder n8n bildest du `data.email`, `data.name` usw. ab.
</Note>

## Die Signatur verifizieren

Die Signatur ist `HMAC-SHA256` über die Zeichenkette `${timestamp}.${rawBody}`, geschlüsselt
mit dem Signaturgeheimnis deines Formulars (`whsec_…`, im Dashboard angezeigt; du kannst es
jederzeit neu generieren). Vergleiche sie mit `X-Formpaste-Signature` (`sha256=<hex>`), und
lehne Anfragen ab, deren `X-Formpaste-Timestamp` für deine Toleranz zu alt ist.

```js theme={null}
import crypto from "node:crypto";

function verify(rawBody, headers, secret, toleranceMs = 5 * 60_000) {
  const ts = Number(headers["x-formpaste-timestamp"]);
  if (!Number.isFinite(ts) || Math.abs(Date.now() - ts) > toleranceMs) return false;

  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  const got = headers["x-formpaste-signature"] ?? "";

  const a = Buffer.from(expected);
  const b = Buffer.from(got);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

Signiere über die **rohen Bytes des Anfrage-Bodys**, vor jeder JSON-Neu-Serialisierung, eine
Neucodierung kann Schlüssel neu anordnen und den Vergleich zerstören.

## Wiederholungen, Backoff & Zustellungsprotokoll

Wenn dein Endpunkt keinen `2xx` zurückgibt, wiederholt Formpaste mit exponentiellem Backoff,
ungefähr **10 s → 60 s → 5 min → 30 min**, bis zu **5 Versuche insgesamt** über etwa 35
Minuten, dann wird die Zustellung als endgültig markiert (Dead-Letter). Eine `4xx`-Antwort
wird als dauerhafter Fehler behandelt und nicht wiederholt, behebe deinen Endpunkt und spiele
sie dann manuell erneut ab. Netzwerkfehler und Timeouts (10 s) werden genauso wiederholt wie
ein `5xx`/`429`.

Dein Endpunkt sollte schnell mit `2xx` antworten und langsame Arbeit asynchron erledigen.

Das Dashboard zeigt ein Zustellungsprotokoll pro Formular (Event, Status, HTTP-Code,
Zeitstempel) mit einer **Replay**-Aktion, um eine vergangene Zustellung erneut zu senden,
sobald dein Endpunkt repariert ist.

## Anforderungen & Limits

* Die URL muss **`https://`** sein, einfaches HTTP wird abgelehnt. Loopback-,
  privates-Netzwerk-, Link-Local- und Cloud-Metadaten-Adressen werden als SSRF-Schutz
  abgelehnt, sowohl beim Speichern als auch erneut vor jedem Versand.
* Eine Webhook-URL pro Formular.

## An Slack / Discord senden

Ein roher Formpaste-Webhook kann nicht direkt an eine Slack- oder Discord-Webhook-URL
posten, diese erwarten ihre eigene Body-Form (Slack: `{"text": …}`, Discord:
`{"content": …}`) und lehnen das obige Envelope ab. Richte deinen Formpaste-Webhook auf eine
Automatisierungsplattform (Zapier, Make, Pipedream, n8n), die das Payload in das Format des
Chat-Dienstes umformt, und lass diese Automatisierung dann an Slack/Discord posten. Es gibt
noch keine eingebaute, direkt einsatzbereite Slack-/Discord-Integration, siehe die
[Slack](/docs/de/guides/slack-notifications)- und [Discord](/docs/de/guides/discord-notifications)-Anleitungen
für das Webhook-Relay-Rezept.

<Note>
  Google Sheets ist eine echte, native Integration: Tabelle
  verbinden und die Zeilen kommen automatisch an, ganz ohne Webhook. "Native"
  Zapier-Trigger, Notion und direkte Airtable-Integrationen sind noch nicht gebaut, dafür
  ist der generische Webhook oben heute der funktionierende Weg.
</Note>

## Free vs. Pro

Free-Formulare können keinen Webhook konfigurieren. Der Versuch, eine `webhook_url` bei Free
zu speichern, liefert:

| Code               | HTTP | Bedeutung                                 |
| ------------------ | ---- | ----------------------------------------- |
| `upgrade_required` | 403  | Diese Funktion erfordert einen Pro-Tarif. |

## Ohne Konfiguration nutzbar

Kein Webhook wird ausgelöst, bis du explizit eine `webhook_url` auf einem Pro-Formular
festlegst, ein neues Formular sendet nichts.
