So prüfst Du, ob ein Webhook wirklich von Safina kommt

Safina kann jeden Webhook signieren. So schaltest Du die Signatur ein, prüfst den Header X-Safina-Signature in Node.js oder Python und sicherst n8n, Zapier und Make ab.

So prüfst Du, ob ein Webhook wirklich von Safina kommt Ratgeber
Karsten Kreh Karsten Kreh
Aktualisiert

Nach jedem Anruf schickt Safina die Zusammenfassung an Deine Webhook-URL. Dein CRM, Dein n8n-Workflow oder Dein Zap macht daraus einen Lead oder eine Aufgabe. Eine Webhook-URL ist aber nur eine Adresse. Wer sie kennt, kann ein gefälschtes „Anruf beendet“-Ereignis an sie schicken.

Eine Signatur löst das Problem. Safina signiert jeden Request mit einem Secret, das nur Du und Safina kennen. Dein Empfänger prüft die Signatur und weiß dann zweierlei: Safina hat den Request gesendet, und niemand hat den Inhalt unterwegs verändert. Ein Zeitstempel in der Signatur verhindert außerdem, dass jemand einen alten Request noch einmal schickt.

Neu bei Webhooks? Lies zuerst Was ist ein Webhook?. Die Einrichtung in der App steht im Leitfaden Safina AI per Webhook mit CRM verknüpfen.

Das brauchst Du

VoraussetzungDetails
Safina-PlanPro oder höher
Ein Webhook in der Safina-AppMit Signatur-Secret (siehe unten)
Ein EmpfängerDein eigener Server, n8n, Zapier oder Make

Webhooks legst Du in der Safina-App an und signierst sie dort. Du kannst sie auch über die Safina REST-API anlegen (POST /webhooks). Die REST-API und API-Schlüssel sind in der Beta: Frühen Zugang bekommst Du auf Anfrage an info@safina.ai, und API-Schlüssel brauchen den Pro-Plan oder höher.

Schritt 1: Signatur einschalten

Die Signatur ist optional. Safina signiert die Requests eines Webhooks nur, wenn er ein Signatur-Secret hat. Ein Secret bekommst Du auf drei Wegen:

  • Neuer Webhook in der App: Öffne in der Safina-App den Tab „Safina AI“, dann „CRM synchronisieren“, und füge einen Webhook hinzu. Schalte vor dem Speichern Zustellungen signieren ein.
  • Bestehender Webhook in der App: Tippe beim Webhook auf Secret erzeugen. Webhooks von vor dieser Funktion bleiben unsigniert, bis Du das tust.
  • REST-API (Beta): Sende "create_signing_secret": true mit POST /webhooks. Die Antwort enthält signing_secret.

Das Secret beginnt mit whsec_. Safina zeigt es nur einmal. Kopiere es und hinterlege es in Deinem Empfänger, wie ein Passwort.

Ein Webhook ohne Secret funktioniert weiter und sendet wie bisher unsignierte Requests. Du kannst auch die Authentifizierung des Webhooks nutzen (auth_type): Safina sendet dann mit jedem Request ein Bearer-Token oder einen API-Schlüssel im Header. Das beweist, dass der Absender das Token kennt. Eine Signatur beweist zusätzlich, dass der Inhalt unverändert und der Request neu ist.

Schritt 2: Den Header verstehen

Jeder signierte Request hat diesen Header:

X-Safina-Signature: t=1794569912,v1=5f2c…e91a
  • t ist die Zeit des Zustellversuchs in Unix-Sekunden.
  • v1 ist der HMAC-SHA256 des Texts <t>.<Raw-Body>, mit Deinem Signatur-Secret als Schlüssel, in Hex.

Dein Empfänger prüft drei Dinge:

  1. Zeit: Lehne den Request ab, wenn t mehr als 300 Sekunden (5 Minuten) vor oder nach Deiner Uhr liegt.
  2. Signatur: Berechne den HMAC über den Raw-Body des Requests, genau so, wie er angekommen ist, bevor Du das JSON parst. Wenn Du das JSON parst und neu schreibst, können sich Leerzeichen oder die Reihenfolge der Schlüssel ändern, und die Signatur passt nicht mehr.
  3. Vergleich: Vergleiche Deinen Wert mit jedem v1-Wert in konstanter Zeit. Ein Treffer reicht.

Warum kann es mehr als ein v1 geben? Wenn Du auf Neues Secret erzeugen tippst, bleibt das alte Secret 24 Stunden gültig. In dieser Zeit hat der Header einen v1-Wert für jedes gültige Secret. So kannst Du Deinen Empfänger umstellen, ohne Ereignisse zu verlieren.

Jeder Zustellversuch hat einen neuen Zeitstempel und eine neue Signatur. Eine Wiederholung sendet dieselbe event_id. Überspringe also eine event_id, die Du schon verarbeitet hast.

Schritt 3: Die Signatur im Code prüfen

Diese Beispiele sind dieselben wie in der API-Referenz (auf Englisch). Wir haben beide mit echten Signaturen aus dem Signatur-Code von Safina getestet.

Node.js

import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the request body exactly as received (a Buffer or a string)
const verifySafinaSignature = (header, rawBody, secret) => {
  const parts = header.split(",").map((part) => part.trim().split("="));
  const timestamp = Number(parts.find(([key]) => key === "t")?.[1]);
  if (!Number.isInteger(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const expected = Buffer.from(
    createHmac("sha256", secret)
      .update(`${timestamp}.`)
      .update(rawBody)
      .digest("hex")
  );
  return parts.some(([key, value]) => {
    const candidate = Buffer.from(value ?? "");
    return (
      key === "v1" &&
      candidate.length === expected.length &&
      timingSafeEqual(candidate, expected)
    );
  });
};

Mit Express liest Du den Body auf dieser Route als Rohdaten, zum Beispiel mit express.raw({ type: "application/json" }). Dann ist req.body ein Buffer. Parse das JSON erst nach der Prüfung:

app.post("/safina-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("X-Safina-Signature") ?? "";
  if (!verifySafinaSignature(header, req.body, process.env.SAFINA_SIGNING_SECRET)) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.body.toString("utf8"));
  // … process the event
  res.status(200).end();
});

Python

import hashlib
import hmac
import time


# raw_body: the request body exactly as received, as bytes
def verify_safina_signature(header: str, raw_body: bytes, secret: str) -> bool:
    parts = [part.strip().split("=", 1) for part in header.split(",")]
    values = [(part[0], part[1]) for part in parts if len(part) == 2]
    timestamp = next((value for key, value in values if key == "t"), "")
    if not (timestamp.isascii() and timestamp.isdigit()):
        return False
    if abs(time.time() - int(timestamp)) > 300:
        return False
    expected = hmac.new(
        secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    return any(
        key == "v1" and hmac.compare_digest(value.encode(), expected.encode())
        for key, value in values
    )

In Flask bekommst Du die Rohdaten mit request.get_data(), in FastAPI mit await request.body().

Die Signatur in Automatisierungstools prüfen

n8n

n8n prüft die Signatur mit einem Code-Node. Sein JavaScript hat das Node.js-Modul crypto: In n8n Cloud ist es verfügbar, in einem selbst gehosteten n8n erlaubst Du es mit der Umgebungsvariable NODE_FUNCTION_ALLOW_BUILTIN=crypto (n8n-Doku).

  1. Wähle im Webhook-Node POST und schalte die Option Raw Body ein. n8n behält den Body dann als Binärdaten. Den Namen des Binärfelds siehst Du in der Ausgabe des Nodes (meist data).
  2. Füge danach einen Code-Node im Modus „Run Once for All Items“ mit diesem Code hinzu:
const crypto = require("crypto");

const secret = "whsec_…"; // your signing secret
const header = $input.first().json.headers["x-safina-signature"] ?? "";
const rawBody = await this.helpers.getBinaryDataBuffer(0, "data");

const parts = header.split(",").map((part) => part.trim().split("="));
const timestamp = Number(parts.find(([key]) => key === "t")?.[1]);
const expected = Buffer.from(
  crypto.createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest("hex")
);
const valid =
  Number.isInteger(timestamp) &&
  Math.abs(Date.now() / 1000 - timestamp) <= 300 &&
  parts.some(
    ([key, value]) =>
      key === "v1" &&
      value?.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(value), expected)
  );

if (!valid) {
  throw new Error("The Safina signature is not valid");
}
return [{ json: JSON.parse(rawBody.toString("utf8")) }];

Ist die Signatur falsch, stoppt der Workflow. Ist sie richtig, bekommen die nächsten Nodes das geparste Ereignis. Der Crypto-Node von n8n kann auch einen HMAC berechnen (Aktion Hmac, Typ SHA256, Encoding HEX). Dafür musst Du aber zuerst den Text <t>.<Raw-Body> bauen und die Werte danach in einem IF-Node vergleichen. Der Code-Node erledigt alles in einem Schritt.

Zapier

Nutze in Zapier den Trigger Webhooks by Zapier → Catch Raw Hook. Anders als Catch Hook liefert er den ungeparsten Body und die Request-Header (Zapier-Hilfe). Webhooks by Zapier braucht den Professional-Plan oder höher.

  1. Füge einen Schritt Code by Zapier (Run JavaScript) hinzu. Ordne unter Input Data rawBody dem Raw-Body des Triggers und signature dem Header X-Safina-Signature zu.
  2. Füge diesen Code ein. Die Standardbibliothek von Node.js und damit crypto ist in Code by Zapier verfügbar (Zapier-Hilfe):
const crypto = require("crypto");

const secret = "whsec_…"; // your signing secret
const parts = inputData.signature.split(",").map((part) => part.trim().split("="));
const timestamp = Number(parts.find(([key]) => key === "t")?.[1]);
const expected = Buffer.from(
  crypto.createHmac("sha256", secret).update(`${timestamp}.`).update(inputData.rawBody).digest("hex")
);
const valid =
  Number.isInteger(timestamp) &&
  Math.abs(Date.now() / 1000 - timestamp) <= 300 &&
  parts.some(
    ([key, value]) =>
      key === "v1" &&
      value?.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(value), expected)
  );

output = { valid };
  1. Füge einen Filter-Schritt hinzu: Nur weitermachen, wenn valid true ist.

Das Secret steht im Code des Schritts. Jeder, der den Zap bearbeiten kann, sieht es also. Catch Raw Hook zerlegt das JSON nicht in Felder. Wenn Du in späteren Schritten einzelne Felder zuordnen willst, parse den Body im selben Code-Schritt und gib die Felder zurück, die Du brauchst.

Make

Make kann die Prüfung mit eingebauten Funktionen machen. Die Funktion sha256(text; [encoding]; [key]) liefert einen HMAC, wenn Du ihr einen Schlüssel gibst (Make-Hilfe). Schalte im Modul Custom webhook JSON pass-through ein (Du bekommst das Original-JSON als Text) und unter den erweiterten Einstellungen Get request headers (Make-Hilfe). Füge dann nach dem Webhook einen Filter mit zwei Bedingungen hinzu:

  • t aus dem Header liegt höchstens 300 Sekunden von formatDate(now; "X") (Unix-Sekunden) entfernt.
  • Der Header X-Safina-Signature enthält v1= gefolgt von sha256(<t>.<Raw-Body>; hex; <Dein Secret>).

Das funktioniert, aber der Filter vergleicht nicht in konstanter Zeit, und Du musst t mit split und get aus dem Header holen. Für die meisten Make-Szenarien ist die Authentifizierung des Webhooks einfacher: Make kann einen Custom Webhook mit einem API-Schlüssel schützen, den es im Header x-make-apikey erwartet (Make-Hilfe). Wähle in Safina die Authentifizierung „API-Schlüssel“, als Ort „Header“ und als Header-Name x-make-apikey, und trage denselben Schlüssel ein.

Häufige Fehler

  • Die Signatur passt nie: Dein Framework hat das JSON vor Deiner Prüfung geparst. Nimm den Raw-Body.
  • Die Signatur passt, aber die Zeitprüfung schlägt fehl: Die Uhr Deines Servers geht falsch. Synchronisiere sie per NTP.
  • Nach einem neuen Secret geht nichts mehr: Das alte Secret gilt nur 24 Stunden. Hinterlege das neue Secret in Deinem Empfänger.
  • Dasselbe Ereignis kommt zweimal: Das ist eine Wiederholung nach einem Timeout oder einem Fehler. Überspringe eine event_id, die Du schon verarbeitet hast.

Fazit

Schalte für jeden Webhook Zustellungen signieren ein, hinterlege das Secret einmal und prüfe X-Safina-Signature, bevor Du ein Ereignis verarbeitest: Zeit, Raw-Body, Vergleich in konstanter Zeit. Dann erreicht kein gefälschter Request Dein CRM. Fragen? Schreib uns an info@safina.ai.

9:41

Safina führte diese Woche 51 Telefonate

46

Vertrauensvoll

4

Verdächtig

1

Gefährlich

Letzte 7 Tage
Filter
EM
Emma Martin 67s 15:30

Möchte das Angebot für die neue Kampagne besprechen und hat Fragen zum Zeitplan.

LS
Laura Wagner 54s 14:45

Fragt nach dem Status der Bestellung und wann die Lieferung kommt.

TH
Tim Hoffmann 34s 13:10

Termin für Projektbesprechung nächste Woche vereinbaren.

Unbekannt 44s 11:30

Gewinnversprechen – wahrscheinlich Spam.

SK
Sophie Meyer 10s 09:15

Reklamation zum letzten Auftrag, bittet um Rückruf.

MM
Martin Neumann 95s 13. Dez

Möchte eine mögliche Zusammenarbeit besprechen.

AR
Anna Richter 85s 13. Dez

Ist Deine Kollegin und möchte über das Projekt sprechen.

JK
Jonas König 42s 12. Dez

Erkundigt sich nach verfügbaren Terminen nächste Woche.

LB
Lina Berg 68s 12. Dez

Hat Fragen zur Rechnung und bittet um Klärung.

Anrufe
Safina
Kontakte
Profil
9:41
Anruf von Emma Martin
12. Dez
11:30
67s
+491723456789

Möchte das Angebot für die neue Kampagne besprechen und hat Fragen zum Zeitplan.

Wichtigste Punkte

  • Rückruf an Emma Martin
  • Fragen zu Zeitplan & Konditionen klären
Zurückrufen
Kontakt bearbeiten

KI-Einblicke

Stimmung der Person Sehr gut

Der Anrufer war kooperativ und hat die benötigten Informationen bereitgestellt.

Dringlichkeit Niedrig

Der Anrufer kann auf eine Rückmeldung warten.

Audio & Transkript

0:16

Hallo, hier spricht Safina AI, die digitale Assistentin von Peter. Wie kann ich Ihnen helfen?

Hallo Safina, hier ist Emma Martin. Ich wollte über das Angebot und den Zeitplan sprechen.

Danke, Emma. Geht es bei Ihrer Entscheidung vor allem um das Standard- oder Pro-Paket für den Launch?

Genau. Wir brauchen das Pro-Paket und möchten nächsten Monat starten, wenn das Onboarding in der ersten Woche möglich ist.

Sag Ciao zu deiner altmodischen Mailbox.

Teste Safina kostenlos und beginne deine Anrufe intelligent zu verwalten.

Safina Support Bot
Safina Support Bot

Hi! 👋 Ich bin der Safina-Support-Assistent. Frag mich alles rund um Einrichtung, Rufumleitung, Tarife oder dein Konto. Hinweis: Deine Eingaben werden gemäß unserer Datenschutzerklärung verarbeitet.

Deine Eingaben werden gemäß unserer Datenschutzerklärung