Anleitungen12 Min. Lesezeit
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.
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.
- Ikiform serialisiert das Event als JSON.
- Es verbindet die Zustellungs-ID, den Unix-Zeitstempel des Versuchs und den Body mit Punkten.
- Es berechnet einen HMAC-SHA256 dieses Strings mit jedem aktiven Secret des Endpunkts.
- Es sendet jedes Ergebnis base64-kodiert und mit dem Präfix v1 im Header webhook-signature.
- Dein Handler liest den Raw Body und die drei Header, bevor er irgendetwas parst.
- Er berechnet denselben HMAC mit seiner Kopie des Secrets.
- Er vergleicht mit konstanter Laufzeit gegen jede v1-Signatur und prüft, ob der Zeitstempel höchstens 5 Minuten abweicht.
- Bei einer Übereinstimmung antwortet er mit 2xx und verarbeitet das Event. Sonst antwortet er mit 400 und tut nichts.
Das Schema im Detail
Ikiform nutzt Standard Webhooks, 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 |
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," + signatureSo prüfst du eine Zustellung, in dieser Reihenfolge:
- Lies den Raw Body als Bytes oder exakten String. Parse ihn noch nicht.
- Lies die drei Header. Fehlt einer, lehne ab.
- 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.
- Entferne
whsec_von deinem Secret und dekodiere den Rest mit base64. Diese 32 Bytes sind der HMAC-Schlüssel, nicht der String selbst. - Berechne den HMAC-SHA256 von
{webhook-id}.{webhook-timestamp}.{raw body}und kodiere ihn mit base64, mit dem Standardalphabet und Padding. - Teile
webhook-signaturean den Leerzeichen. Teile jeden Eintrag am ersten Komma; überspringe alles, dessen Version nichtv1ist. - Vergleiche jeden
v1-Wert mit deinem in konstanter Laufzeit. Stimmt einer überein, ist die Zustellung echt. - 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:
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 \
| base64Zwei 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:
webhook-id: dlv_01k7v2d4n8r2p6s0w3y5b7c9e1
webhook-timestamp: 1792300000
webhook-signature: v1,Duq+Iuh0xbRZyS+C/TO5W1hcGLGwg+ifBiWiYkq15qA= v1,gfHP3s7ANdIpnM7gewtWtSvTrHgmsbEFgCaeRRR0Eak=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:
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 ergänzt Deduplizierung und after().
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;
}
}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.
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;
}
};Eine Server-Route: dieselbe dateibasierte Route, mit einer Funktion server.handlers.POST, die den Standard-Request erhält.
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;
}
},
},
},
});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.
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;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;
}
}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.
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;
}
};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.
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 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.
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:
- Vor der Rotation trägt jede Zustellung eine Signatur, erstellt mit dem alten Secret.
- Du wählst Roll signing secret und kopierst das neue Secret.
- 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.
- Nach 24 Stunden tragen Zustellungen nur noch die neue Signatur, und ein Server mit dem alten Secret lehnt sie ab.
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-idoder dieiddes 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
2xxzurü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.fieldsenthä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. 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, direkt oder über ein Automatisierungstool wie Zapier oder n8n. Ein Workspace kann 3 Endpunkte haben, jeder mit eigenem Secret. Den vollständigen Payload, den Retry-Zeitplan und das Zustellungsprotokoll findest du unter Webhooks.
Wähle deinen Fall
Jede Sprachseite hat idiomatische Handler, eine Bibliotheksoption und einen Unit-Test, der den Testvektor von dieser Seite ausführt.
- Node.jsPrü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.
- PythonPrüfe Webhook-Signaturen von Ikiform in Flask, FastAPI und Django, mit dem Paket standardwebhooks oder der Standardbibliothek, plus einem Test mit pytest.
- Go net/httpVerifiziere 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.
- PHPVerifiziere Ikiform-Webhook-Signaturen in reinem PHP mit hash_hmac und hash_equals und in Laravel mit der Standard-Webhooks-Bibliothek und einer CSRF-Ausnahme.
- RubyVerifiziere 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.
- Lass den Testvektor durch deinen Code laufen: Er sollte die erwartete Signatur erzeugen, den Header mit zwei Signaturen akzeptieren und den formatierten Body ablehnen.
- Öffne im Dashboard Webhooks, füge die öffentliche
https:-URL deines Endpunkts hinzu und kopiere daswhsec_-Secret in die Umgebung deines Servers. - Wähle Send test auf der Endpunktseite. Das Zustellungsprotokoll zeigt dein
204, und das Event landet in deinen Logs. - Ändere ein Zeichen des Secrets auf deinem Server und sende einen weiteren Test. Das Protokoll zeigt
400mitno_matching_signature. - Sende das Formular wirklich ab und prüfe, ob die Zustellung von
submission.createderfolgreich 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
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.
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.
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.
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.
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.
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.
Hängst du bei einem Schritt fest oder willst eine Anleitung für deinen Fall? Schreib an hello@ikiform.com.