# Webhook-Signaturen von Formularen prüfen

> Prüfe, ob ein Formular-Webhook wirklich von Ikiform kommt: das HMAC-SHA256-Schema von Standard Webhooks, ein Testvektor, Raw Bodys, Zeitstempel und Secret-Rotation.

Veröffentlicht am 2026-10-25 · https://ikiform.com/de/guides/webhook-signature-verification

Berechne die Signatur selbst neu und vergleiche sie mit der im Request. Ikiform folgt Standard Webhooks: Die Signatur ist ein base64-kodierter HMAC-SHA256 aus `webhook-id`, `webhook-timestamp` und dem Raw Body, mit Punkten verbunden, als Schlüssel dient der base64-dekodierte Teil deines `whsec_`-Secrets, gesendet als `v1,<signature>`. Akzeptiere den Request nur, wenn eine `v1`-Signatur in einem Vergleich mit konstanter Laufzeit übereinstimmt und der Zeitstempel höchstens 5 Minuten abweicht.

Ein Webhook-Endpunkt ist eine öffentliche URL, die POST-Requests von jedem annimmt. Ohne Signaturprüfung kann jeder, der die URL kennt, gefälschte Einsendungen in dein CRM, deine Datenbank oder den Chat deines Teams schicken. Die Prüfung sind ein paar Zeilen Code, und genau darum, diese Zeilen exakt richtig hinzubekommen, geht es in dieser Anleitung.

## So funktioniert das Signieren

Ikiform und dein Server teilen ein Secret, das einmal angezeigt wird, wenn du den Endpunkt hinzufügst. Vor jedem Zustellversuch hasht Ikiform den Request mit diesem Secret und legt das Ergebnis in einen Header. Dein Server wiederholt die Berechnung mit dem, was er empfangen hat. Nur wer das Secret kennt, kann einen passenden Wert erzeugen, und jede Änderung an Body, ID oder Zeitstempel macht ihn ungültig.

Diagramm: Wie Ikiform eine Webhook-Zustellung signiert und wie dein Server sie prüft

1. Ikiform serialisiert das Event als JSON.
2. Es verbindet die Zustellungs-ID, den Unix-Zeitstempel des Versuchs und den Body mit Punkten.
3. Es berechnet einen HMAC-SHA256 dieses Strings mit jedem aktiven Secret des Endpunkts.
4. Es sendet jedes Ergebnis base64-kodiert und mit dem Präfix v1 im Header webhook-signature.
5. Dein Handler liest den Raw Body und die drei Header, bevor er irgendetwas parst.
6. Er berechnet denselben HMAC mit seiner Kopie des Secrets.
7. Er vergleicht mit konstanter Laufzeit gegen jede v1-Signatur und prüft, ob der Zeitstempel höchstens 5 Minuten abweicht.
8. Bei einer Übereinstimmung antwortet er mit 2xx und verarbeitet das Event. Sonst antwortet er mit 400 und tut nichts.

Mit dem Request reist nichts Geheimes. Die Signatur beweist, dass der Absender das Secret kannte und die Bytes unterwegs nicht verändert wurden.

## Das Schema im Detail

Ikiform nutzt [Standard Webhooks](https://www.standardwebhooks.com), eine offene Spezifikation, die mehrere Webhook-Anbieter teilen. Jede Zustellung trägt drei Header:

| Header | Wert | Hinweise |
| --- | --- | --- |
| `webhook-id` | `dlv_...` | Die Zustellungs-ID. Bei jedem erneuten Versuch gleich, dient also auch als Idempotenzschlüssel |
| `webhook-timestamp` | Unix-Sekunden, etwa `1792300000` | Wann dieser Versuch signiert wurde. Bei jedem erneuten Versuch neu |
| `webhook-signature` | `v1,` plus base64 | Ein oder mehrere durch Leerzeichen getrennte Werte. Zwei, während ein Secret rotiert wird |

`Was signiert wird`:

```text
webhook-id:        dlv_01k7v2d4n8r2p6s0w3y5b7c9e1
webhook-timestamp: 1792300000
raw body:          {"id":"dlv_01k7...","type":"endpoint.test",...}

signed content = webhook-id + "." + webhook-timestamp + "." + raw body
key            = base64-decode(secret without "whsec_")
signature      = base64(HMAC-SHA256(key, signed content))
header         = "v1," + signature
```

So prüfst du eine Zustellung, in dieser Reihenfolge:

1. Lies den Raw Body als Bytes oder exakten String. Parse ihn noch nicht.
2. Lies die drei Header. Fehlt einer, lehne ab.
3. Prüfe, ob der Zeitstempel eine ganze Zahl ist und höchstens 5 Minuten von deiner Uhr abweicht, in beide Richtungen. Wenn nicht, lehne ab.
4. Entferne `whsec_` von deinem Secret und dekodiere den Rest mit base64. Diese 32 Bytes sind der HMAC-Schlüssel, nicht der String selbst.
5. Berechne den HMAC-SHA256 von `{webhook-id}.{webhook-timestamp}.{raw body}` und kodiere ihn mit base64, mit dem Standardalphabet und Padding.
6. Teile `webhook-signature` an den Leerzeichen. Teile jeden Eintrag am ersten Komma; überspringe alles, dessen Version nicht `v1` ist.
7. Vergleiche jeden `v1`-Wert mit deinem in konstanter Laufzeit. Stimmt einer überein, ist die Zustellung echt.
8. Parse erst jetzt das JSON und reagiere darauf.

## Ein Testvektor

Bevor du einen Handler deployst, beweise, dass er den richtigen Wert berechnet. Diese Eingaben sind ein echter `endpoint.test`-Zustellungsbody, signiert mit einem erfundenen Secret. Dein Code muss exakt die Signatur in der letzten Zeile erzeugen. Die Anleitungen für jede Sprache enthalten einen Unit-Test, der sie verwendet.

| Eingabe | Wert |
| --- | --- |
| Secret | `whsec_Fg8c6hm1Gt41v5RSPKm72LggDx8S1mlWkbBYpHPAnpw=` |
| `webhook-id` | `dlv_01k7v2d4n8r2p6s0w3y5b7c9e1` |
| `webhook-timestamp` | `1792300000` |
| Raw Body | Exakt die Bytes im Skript unten, ohne Leerzeichen oder Zeilenumbruch |
| Erwartete `webhook-signature` | `v1,Duq+Iuh0xbRZyS+C/TO5W1hcGLGwg+ifBiWiYkq15qA=` |

Du kannst es ohne Code prüfen. Das hier läuft in einem Terminal unter macOS oder Linux und gibt den base64-Teil der Signatur aus:

`Terminal`:

```bash
SECRET='whsec_Fg8c6hm1Gt41v5RSPKm72LggDx8S1mlWkbBYpHPAnpw='
ID='dlv_01k7v2d4n8r2p6s0w3y5b7c9e1'
TS='1792300000'
BODY='{"id":"dlv_01k7v2d4n8r2p6s0w3y5b7c9e1","type":"endpoint.test","timestamp":"2026-10-17T12:00:00.000Z","api_version":"2026-10-17","data":{"endpoint_id":"whk_01k7v2e1b3d5f7h9k1m3n5q7s9","message":"This is a test event from Ikiform."}}'

KEY=$(printf '%s' "${SECRET#whsec_}" | base64 -d | xxd -p -c 256)
printf '%s.%s.%s' "$ID" "$TS" "$BODY" \
  | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$KEY" -binary \
  | base64
```

Zwei weitere Fälle verdienen je einen Test, weil sie die zwei häufigsten echten Fehler sind:

| Änderung | Signatur | Sollte dein Handler sie akzeptieren? |
| --- | --- | --- |
| Derselbe Body, formatiert mit zwei Leerzeichen Einrückung | `v1,se0TDhknrG0hYMvCId83HnpZfJcELG3ozxFJktRUHqY=` | Nein. Gleiches JSON, andere Bytes |
| `webhook-timestamp` eine Sekunde später | `v1,oBfWyLDPfrwVSo40e6b7B7+LmKkZX7OqT88SsrpY6ks=` | Nein. Der Zeitstempel ist signiert |
| Während einer Rotation mit einem zweiten Secret signiert | `v1,gfHP3s7ANdIpnM7gewtWtSvTrHgmsbEFgCaeRRR0Eak=` | Ja, wenn der Header auch einen Wert enthält, zu dem dein Secret passt |

Während einer Secret-Rotation trägt der Header beide Werte. Dein Handler muss das hier akzeptieren, egal welches der beiden Secrets er hat:

`Zwei Signaturen`:

```http
webhook-id: dlv_01k7v2d4n8r2p6s0w3y5b7c9e1
webhook-timestamp: 1792300000
webhook-signature: v1,Duq+Iuh0xbRZyS+C/TO5W1hcGLGwg+ifBiWiYkq15qA= v1,gfHP3s7ANdIpnM7gewtWtSvTrHgmsbEFgCaeRRR0Eak=
```

> Der Zeitstempel im Vektor ist fest, daher lehnt ein echter Verifier ihn als zu alt ab. Tests müssen eine feste aktuelle Zeit übergeben, wie es die Anleitungen für jede Sprache tun. Der [Signatur-Verifier](https://ikiform.com/de/docs/webhooks.md#verifier) auf der Webhooks-Seite prüft beliebige Secrets, Header und Bodys im Browser.

## Wege zur Prüfung

| Ansatz | Stark bei | Nachteile |
| --- | --- | --- |
| `verifyWebhook` aus `@ikiform/sdk/webhooks` | Ein Aufruf, der alles prüft und ein typisiertes Event zurückgibt. Läuft auf Node, Workers, Deno und Bun | Nur JavaScript und TypeScript |
| Eine Standard-Webhooks-Bibliothek | Gepflegter Code für Python, Go, PHP, Ruby, JavaScript, Rust, Java, C# und Elixir | Eine Abhängigkeit mehr. Prüfe, wie jede die Header übergeben haben will; manche unterscheiden Groß- und Kleinschreibung |
| Dein eigener HMAC-Code | Keine Abhängigkeit, etwa 20 Zeilen, und du kannst jeden Zweig testen | Leicht auf subtile Weise falsch: String-Vergleich, falsche Schlüssel-Bytes oder ein geparster Body |
| Stattdessen ein geheimer Pfad oder Query-Token | Einfach | Leakt über Logs, Proxys und Error-Tracker, beweist nichts über den Body und kann nicht ablaufen. Kein Ersatz |

Ikiform signiert jede Zustellung, und die Signatur ist der unterstützte Weg, eine zu authentifizieren. Die Doku veröffentlicht keine feste Liste sendender IP-Adressen, daher ist eine IP-Allowlist keine Prüfung, auf die du dich verlassen kannst.

## In deinem Framework prüfen

Für JavaScript-Stacks übernimmt das SDK die Arbeit. Zwischen Frameworks ändert sich, wo die Route liegt und wie du an den Body kommst, bevor ihn etwas parst. Wähle deins:

**Next.js**

Ein Route Handler im App Router. `request.text()` ist der Raw Body; ein vorheriger Aufruf von `request.json()` würde ihn verlieren. Die [Anleitung für den eigenen Server](https://ikiform.com/blog/form-submissions-to-your-own-server.md) ergänzt Deduplizierung und `after()`.

`app/api/webhooks/ikiform/route.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

export async function POST(request: Request) {
  const payload = await request.text();
  try {
    const event = await verifyWebhook({
      payload,
      headers: request.headers,
      secret: process.env.IKIFORM_WEBHOOK_SECRET!,
    });
    console.info(event.type, request.headers.get("webhook-id"));
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
}
```

**Astro**

Ein Endpunkt mit einem `POST`-Export. Er braucht On-Demand-Rendering, also behalte `prerender = false` und einen Adapter. `getSecret` aus `astro:env/server` liest `IKIFORM_WEBHOOK_SECRET` zur Laufzeit und nur auf dem Server; `import.meta.env` würde es in den Build einbacken.

`src/pages/api/webhooks/ikiform.ts`:

```ts
import type { APIRoute } from "astro";
import { getSecret } from "astro:env/server";
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

export const prerender = false;

export const POST: APIRoute = async ({ request }) => {
  const payload = await request.text();
  try {
    const event = await verifyWebhook({
      payload,
      headers: request.headers,
      secret: getSecret("IKIFORM_WEBHOOK_SECRET") ?? "",
    });
    console.info(event.type, event.id);
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
};
```

**TanStack Start**

Eine Server-Route: dieselbe dateibasierte Route, mit einer Funktion `server.handlers.POST`, die den Standard-`Request` erhält.

`src/routes/api/webhooks/ikiform.ts`:

```ts
import { createFileRoute } from "@tanstack/react-router";
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

export const Route = createFileRoute("/api/webhooks/ikiform")({
  server: {
    handlers: {
      POST: async ({ request }) => {
        const payload = await request.text();
        try {
          const event = await verifyWebhook({
            payload,
            headers: request.headers,
            secret: process.env.IKIFORM_WEBHOOK_SECRET!,
          });
          console.info(event.type, event.id);
          return new Response(null, { status: 204 });
        } catch (error) {
          if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
          throw error;
        }
      },
    },
  },
});
```

**React Router**

Eine Resource Route: ein Modul mit einer `action` und ohne Default-Export, registriert in `routes.ts`. Sie antwortet Ikiform direkt, statt eine Seite zu rendern.

`app/routes.ts`:

```ts
import { type RouteConfig, index, route } from "@react-router/dev/routes";

export default [
  index("routes/home.tsx"),
  route("webhooks/ikiform", "routes/webhooks.ikiform.ts"),
] satisfies RouteConfig;
```

`app/routes/webhooks.ikiform.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";
import type { Route } from "./+types/webhooks.ikiform";

export async function action({ request }: Route.ActionArgs) {
  if (request.method !== "POST") return new Response(null, { status: 405 });
  const payload = await request.text();
  try {
    const event = await verifyWebhook({
      payload,
      headers: request.headers,
      secret: process.env.IKIFORM_WEBHOOK_SECRET!,
    });
    console.info(event.type, event.id);
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
}
```

**SvelteKit**

Ein `+server.ts`-Endpunkt. Der eingebaute CSRF-Check von SvelteKit blockiert nur Cross-Site-Formular-Content-Types, und Ikiform sendet `application/json`, also ist keine Konfigurationsänderung nötig. Deklariere `IKIFORM_WEBHOOK_SECRET` in `src/env.ts` mit `defineEnvVars`, damit `$app/env/private` es lesen kann.

`src/routes/webhooks/ikiform/+server.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";
import { IKIFORM_WEBHOOK_SECRET } from "$app/env/private";
import type { RequestHandler } from "./$types";

export const POST: RequestHandler = async ({ request }) => {
  const payload = await request.text();
  try {
    const event = await verifyWebhook({ payload, headers: request.headers, secret: IKIFORM_WEBHOOK_SECRET });
    console.info(event.type, event.id);
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
};
```

**Nuxt**

Eine Nitro-Server-Route. `readRawBody` gibt den Body als String zurück, bevor ihn etwas parst. Setze `NUXT_IKIFORM_WEBHOOK_SECRET` und deklariere `ikiformWebhookSecret` unter `runtimeConfig` in `nuxt.config.ts`.

`server/api/webhooks/ikiform.post.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

export default defineEventHandler(async (event) => {
  const payload = (await readRawBody(event, "utf8")) ?? "";
  const { ikiformWebhookSecret } = useRuntimeConfig(event);
  try {
    const delivery = await verifyWebhook({
      payload,
      headers: getRequestHeaders(event),
      secret: ikiformWebhookSecret,
    });
    console.info(delivery.type, delivery.id);
    setResponseStatus(event, 204);
    return null;
  } catch (error) {
    if (error instanceof WebhookVerificationError) {
      throw createError({ status: 400, message: error.reason });
    }
    throw error;
  }
});
```

Jeder Handler antwortet bei Erfolg mit `204` und sonst mit `400` samt Fehlergrund. Erledige langsame Arbeit nach der Antwort; Ikiform wartet 15 Sekunden, bevor es einen Versuch als fehlgeschlagen zählt.

## Warum der Raw Body zählt

Die Signatur deckt Bytes ab, nicht Bedeutung. `{"a":1}` und `{ "a": 1 }` sind dasselbe JSON, aber unterschiedliche Bytes, daher passt ein Body, der geparst und erneut serialisiert wurde, fast nie. Dasselbe gilt für alles, was den Body auf dem Weg hinein anfasst: Middleware, die JSON parst, ein Proxy, der neu kodiert, oder ein Framework, das dir ein Objekt statt eines Strings gibt.

Lies den Body einmal als Bytes oder String, prüfe ihn und parse dann genau diesen String. Jede Sprachanleitung zeigt, wo der Raw Body in ihren Frameworks liegt, und die [Anleitung für den eigenen Server](https://ikiform.com/blog/form-submissions-to-your-own-server.md) hat eine Tabelle für die gängigen Node- und Python-Frameworks.

## Mit konstanter Laufzeit vergleichen

Ein normaler String-Vergleich stoppt beim ersten abweichenden Zeichen. Theoretisch kann ein Angreifer, der Tausende Requests misst, herausfinden, wie viele führende Zeichen eines Rateversuchs stimmen, und so Zeichen für Zeichen eine gültige Signatur bauen. Ein Vergleich mit konstanter Laufzeit dauert gleich lang, egal wie viel übereinstimmt. Jede Sprache hat einen:

| Sprache | Vergleich mit konstanter Laufzeit |
| --- | --- |
| Node.js | `crypto.timingSafeEqual(a, b)`, das bei unterschiedlicher Länge eine Exception wirft, also prüfe zuerst die Länge |
| Python | `hmac.compare_digest(a, b)` |
| Go | `hmac.Equal(a, b)` |
| PHP | `hash_equals($known, $user)` |
| Ruby | `OpenSSL.secure_compare(a, b)` oder `ActiveSupport::SecurityUtils.secure_compare` in Rails |

## Zeitstempel und Replays

Ein einmal mitgeschnittener gültiger Request bleibt für immer gültig, außer etwas lässt ihn ablaufen. Der signierte Zeitstempel ist dieses Etwas: Lehne jede Zustellung ab, die mehr als 5 Minuten von deiner Uhr abweicht, in beide Richtungen. Weil Ikiform jeden Versuch mit frischem Zeitstempel neu signiert, besteht ein erneuter Versuch auch drei Tage später; nur ein kopierter alter Request scheitert.

Innerhalb dieser 5 Minuten ist ein Replay weiterhin möglich, ebenso ein echtes Duplikat: Ikiform versucht es erneut, wenn es nicht rechtzeitig ein `2xx` bekommt, auch wenn dein Handler fertig war. Speichere `webhook-id` mit einem Unique Constraint und überspringe IDs, die du schon verarbeitet hast. Das schließt beides aus.

> Halte die Serveruhr synchron. Eine Uhr, die um mehr als 5 Minuten abweicht, lehnt jede Zustellung mit `timestamp_too_old` oder `timestamp_too_new` ab. Verwaltete Plattformen halten Uhren meist synchron; auf deiner eigenen VM lass NTP laufen.

## Das Secret rotieren

Rotiere das Secret, wenn jemand geht, der es kannte, wenn es geloggt oder committet worden sein könnte, oder nach Zeitplan. **Roll signing secret** (Signatur-Secret rotieren) auf der Endpunktseite erzeugt ein neues, und 24 Stunden lang wird jede Zustellung mit beiden signiert:

Diagramm: Was webhook-signature vor, während und nach einer Secret-Rotation enthält

1. Vor der Rotation trägt jede Zustellung eine Signatur, erstellt mit dem alten Secret.
2. Du wählst Roll signing secret und kopierst das neue Secret.
3. In den nächsten 24 Stunden trägt jede Zustellung zwei Signaturen, sodass ein Server mit einem der beiden Secrets sie akzeptiert. Deploye das neue Secret in diesem Zeitfenster.
4. Nach 24 Stunden tragen Zustellungen nur noch die neue Signatur, und ein Server mit dem alten Secret lehnt sie ab.

Dein Handler braucht dafür keinen speziellen Code, solange er jeden v1-Eintrag prüft statt nur den ersten.

Ist nach 24 Stunden noch das alte Secret deployt, schlägt jede Zustellung fehl. Ikiform versucht jede 8-mal über etwa drei Tage erneut. Sobald du das richtige Secret deployt hast, nutze also **Retry** (Erneut versuchen) im Zustellungsprotokoll für alles, was bereits aufgegeben wurde.

## Nach bestandener Prüfung

Eine gültige Signatur beweist, dass der Request von Ikiform kam und nicht verändert wurde. Sie beweist nicht, dass der Inhalt sicher oder neu ist. Drei Gewohnheiten erledigen den Rest:

- **Wiederholungen überspringen.** Speichere `webhook-id` oder die `id` des Payloads, die denselben Wert hat, mit einem Unique Constraint. Ein erneuter Versuch einer bereits verarbeiteten Zustellung wird so zum No-op.
- **Erst antworten, dann arbeiten.** Gib `2xx` zurück, sobald das Event gespeichert oder in die Queue gestellt ist. Ikiform zählt alles, was länger als 15 Sekunden dauert, als Fehlschlag und versucht es erneut, was das Duplikatproblem zurückbringt.
- **Felder als nicht vertrauenswürdig behandeln.** `data.fields` enthält, was ein Besucher in ein öffentliches Formular getippt hat. Escape es, bevor du es in HTML, einer Chatnachricht oder einer E-Mail darstellst, und übergib es nie an eine Shell oder unparametrisiert an eine Query.

### Testen vor dem Livegang

Ikiform stellt nur an öffentliche `https:`-URLs auf Port 443 zu, daher kann `localhost` keinen Webhook empfangen. Starte einen Tunnel wie `cloudflared tunnel --url http://localhost:3000` und füge die ausgegebene URL während der Arbeit als zweiten Endpunkt hinzu. **Send test** (Test senden) auf der Endpunktseite sendet einmal ein `endpoint.test`-Event, ohne erneute Versuche, signiert genau wie eine echte Zustellung. Lösche den Test-Endpunkt, wenn du fertig bist; er hat sein eigenes Secret, also ändert sich sonst nichts.

Wenn eine Zustellung fehlschlägt und du nicht siehst, warum, kopiere ihre Header und ihren Body aus deinen Logs in den [Signatur-Verifier](https://ikiform.com/de/docs/webhooks.md#verifier). Er führt dieselbe Prüfung im Browser aus und sagt dir, welcher Schritt fehlgeschlagen ist.

## Fünf Arten, wie die Prüfung schiefgeht

| Fehler | Symptom | Lösung |
| --- | --- | --- |
| Den ganzen `whsec_...`-String als Schlüssel nutzen | `no_matching_signature` bei jeder Zustellung | Entferne `whsec_` und dekodiere dann mit base64. Der Schlüssel ist 32 Bytes lang |
| Einen geparsten Body prüfen | `no_matching_signature`, manchmal nur bei manchen Payloads | Prüfe den Raw Body; parse danach |
| Nur die erste Signatur prüfen | Fehlschläge, die direkt nach einer Rotation beginnen | Iteriere über jeden durch Leerzeichen getrennten `v1`-Eintrag |
| Hex statt base64 | `no_matching_signature` | Standard Webhooks nutzt Standard-base64 mit Padding, nicht Hex oder base64url |
| Header mit exakter Schreibweise nachschlagen | `missing_headers` hinter manchen Proxys | Header-Namen unterscheiden nicht zwischen Groß- und Kleinschreibung. Ikiform sendet sie kleingeschrieben; schlag sie ohne Beachtung der Schreibweise nach |

Es gibt keine Ein-Klick-Integrationen. Jedes Tool erreichst du über einen signierten [Webhook](https://ikiform.com/de/docs/webhooks.md), direkt oder über ein Automatisierungstool wie [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) oder [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md). Ein Workspace kann 3 Endpunkte haben, jeder mit eigenem Secret. Den vollständigen Payload, den Retry-Zeitplan und das Zustellungsprotokoll findest du unter [Webhooks](https://ikiform.com/de/docs/webhooks.md).

## Wähle deinen Fall

Jede Sprachseite hat idiomatische Handler, eine Bibliotheksoption und einen Unit-Test, der den Testvektor von dieser Seite ausführt.

- [Node.js](https://ikiform.com/de/guides/webhook-signature-verification/nodejs.md): Prüfe Webhook-Signaturen von Ikiform in Node.js: Raw Bodys in Express und Fastify, das SDK, das Paket standardwebhooks und eine getestete Version mit node:crypto.
- [Python](https://ikiform.com/de/guides/webhook-signature-verification/python.md): Prüfe Webhook-Signaturen von Ikiform in Flask, FastAPI und Django, mit dem Paket standardwebhooks oder der Standardbibliothek, plus einem Test mit pytest.
- [Go net/http](https://ikiform.com/de/guides/webhook-signature-verification/go.md): Verifiziere Ikiform-Webhook-Signaturen in Go mit net/http: Lies den Body einmal, prüf ihn mit crypto/hmac, teste ihn oder nutze die Standard-Webhooks-Bibliothek.
- [PHP](https://ikiform.com/de/guides/webhook-signature-verification/php.md): Verifiziere Ikiform-Webhook-Signaturen in reinem PHP mit hash_hmac und hash_equals und in Laravel mit der Standard-Webhooks-Bibliothek und einer CSRF-Ausnahme.
- [Ruby](https://ikiform.com/de/guides/webhook-signature-verification/ruby.md): Verifiziere Ikiform-Webhook-Signaturen in Sinatra mit OpenSSL und einem Minitest-Test und in Rails mit dem Gem standardwebhooks und request.raw_post.

## Prüfen, ob es funktioniert

Mach das zuerst auf einem Staging-Endpunkt und dann noch einmal in Produktion. Ikiform hat keinen kostenlosen Tarif. Testen ist kostenlos: Ein Workspace ohne Tarif nimmt 50 Testeinsendungen pro Monat von `localhost` an. Für den Livegang wähle Pro ab 6 $/Monat für 500 Einsendungen oder zahle einmalig: 10.000 Einsendungen für 49 $ oder 50.000 Einsendungen für 149 $, mit Guthaben, das nie verfällt. Siehe [Preise](https://ikiform.com/de/pricing).

1. Lass den Testvektor durch deinen Code laufen: Er sollte die erwartete Signatur erzeugen, den Header mit zwei Signaturen akzeptieren und den formatierten Body ablehnen.
2. Öffne im Dashboard **Webhooks**, füge die öffentliche `https:`-URL deines Endpunkts hinzu und kopiere das `whsec_`-Secret in die Umgebung deines Servers.
3. Wähle **Send test** auf der Endpunktseite. Das Zustellungsprotokoll zeigt dein `204`, und das Event landet in deinen Logs.
4. Ändere ein Zeichen des Secrets auf deinem Server und sende einen weiteren Test. Das Protokoll zeigt `400` mit `no_matching_signature`.
5. Sende das Formular wirklich ab und prüfe, ob die Zustellung von `submission.created` erfolgreich ist.

## Fehlerbehebung

| Problem | Was zu tun ist |
| --- | --- |
| `400 no_matching_signature` | Falsches Secret, ein geparster Body oder ein Schlüssel, der nicht base64-dekodiert wurde. Lass den Testvektor durch denselben Codepfad laufen, um zu sehen, was davon. |
| `400 timestamp_too_old` oder `timestamp_too_new` | Die Serveruhr geht falsch, oder du spielst einen gespeicherten Request von Hand erneut ab. Synchronisiere die Uhr und nutze **Retry** im Zustellungsprotokoll, das neu signiert. |
| `400 missing_headers` | Ein Proxy oder Framework hat die `webhook-*`-Header verworfen, oder der Request kam nicht von Ikiform. Logge jeden Header-Namen, den dein Handler sieht. |
| Klappt bei Test-Events, scheitert bei echten Einsendungen | Echte Payloads enthalten Nicht-ASCII-Zeichen und längeren Text. Dein Code dekodiert oder kodiert den Body vor der Prüfung neu. Prüfe die exakten Bytes. |
| Jede Zustellung scheitert direkt nach der Secret-Rotation | Der Handler prüft nur die erste Signatur, oder die 24 Stunden sind mit dem alten Secret im Deployment verstrichen. Prüfe jeden `v1`-Eintrag und deploye das neue Secret. |
| Das Zustellungsprotokoll zeigt einen Timeout, aber dein Handler lief | Ikiform wartet 15 Sekunden. Antworte zuerst, erledige langsame Arbeit danach und überspringe wiederholte `webhook-id`-Werte. |

## Fragen

### Wie prüfe ich eine Webhook-Signatur?

Berechne den HMAC-SHA256 aus Webhook-ID, Zeitstempel und Raw Body, mit Punkten verbunden, mit dem base64-dekodierten Teil deines whsec_-Secrets als Schlüssel neu. Kodiere das Ergebnis mit base64 und vergleiche es in konstanter Laufzeit mit jedem v1-Wert im Header webhook-signature.

### Warum passt meine Webhook-Signatur nie?

Fast immer ist es eins von drei Dingen: Der Body wurde vor der Prüfung geparst und neu serialisiert, der ganze whsec_-String wurde statt seiner base64-dekodierten Bytes als Schlüssel genutzt, oder das Secret gehört zu einem anderen Endpunkt.

### Was ist Standard Webhooks?

Eine offene Spezifikation zum Signieren von Webhooks mit HMAC-SHA256 und den Headern webhook-id, webhook-timestamp und webhook-signature. Ikiform folgt ihr, daher kann jede Standard-Webhooks-Bibliothek Zustellungen von Ikiform prüfen.

### Warum alte Zeitstempel ablehnen?

Sonst bliebe ein mitgeschnittener Request für immer gültig. Wenn du alles ablehnst, was mehr als 5 Minuten von deiner Uhr abweicht, stoppst du Replays alter Requests, und weil Ikiform jeden erneuten Versuch mit frischem Zeitstempel signiert, bestehen echte Retries trotzdem.

### Was passiert mit Zustellungen, wenn ich das Secret rotiere?

24 Stunden lang wird jede Zustellung mit dem alten und dem neuen Secret signiert, sodass du das neue deployen kannst, ohne Events zu verlieren. Danach funktioniert nur noch das neue Secret.

### Sollte mein Handler bei einer falschen Signatur 401 oder 400 zurückgeben?

Für Ikiform ist beides in Ordnung; jeder Status außer 2xx zählt als Fehlschlag und wird erneut versucht. Gib nicht 410 zurück, das deaktiviert den Endpunkt sofort.

## Weitere Anleitungen

- [Node.js](https://ikiform.com/de/guides/webhook-signature-verification/nodejs.md): Prüfe Webhook-Signaturen von Ikiform in Node.js: Raw Bodys in Express und Fastify, das SDK, das Paket standardwebhooks und eine getestete Version mit node:crypto.
- [Python](https://ikiform.com/de/guides/webhook-signature-verification/python.md): Prüfe Webhook-Signaturen von Ikiform in Flask, FastAPI und Django, mit dem Paket standardwebhooks oder der Standardbibliothek, plus einem Test mit pytest.
- [Go net/http](https://ikiform.com/de/guides/webhook-signature-verification/go.md): Verifiziere Ikiform-Webhook-Signaturen in Go mit net/http: Lies den Body einmal, prüf ihn mit crypto/hmac, teste ihn oder nutze die Standard-Webhooks-Bibliothek.
- [PHP](https://ikiform.com/de/guides/webhook-signature-verification/php.md): Verifiziere Ikiform-Webhook-Signaturen in reinem PHP mit hash_hmac und hash_equals und in Laravel mit der Standard-Webhooks-Bibliothek und einer CSRF-Ausnahme.
- [Ruby](https://ikiform.com/de/guides/webhook-signature-verification/ruby.md): Verifiziere Ikiform-Webhook-Signaturen in Sinatra mit OpenSSL und einem Minitest-Test und in Rails mit dem Gem standardwebhooks und request.raw_post.
- [Warteliste für ein neues SaaS](https://ikiform.com/de/guides/waitlist.md): Baue eine Pre-Launch-Warteliste auf deiner eigenen Landingpage: das Formular, Spamschutz, Duplikate, Bestätigungs-E-Mails und ein sauberer Export am Launch-Tag.
- [Kontaktformular auf einer statischen Site](https://ikiform.com/de/guides/contact-form-static-site.md): Bau ein funktionierendes Kontaktformular in eine statische Site ohne Server ein: wohin der POST geht, Spam, Antworten per E-Mail und Code für jeden Host und jedes Framework.
- [Ein Formular an deine E-Mail senden](https://ikiform.com/de/guides/send-form-to-email.md): Bekomm jede Formulareinsendung ohne Mailserver in dein Postfach: warum mailto: und PHPs mail() scheitern und ein Setup, das Spam filtert und dich antworten lässt.
