Jak sprawdzić, czy webhook naprawdę pochodzi od Safiny

Safina może podpisywać każdy wysyłany webhook. Dowiedz się, jak włączyć podpis, sprawdzić nagłówek X-Safina-Signature w Node.js lub Pythonie i weryfikować połączenia w n8n, Zapier i Make.

Jak sprawdzić, czy webhook naprawdę pochodzi od Safiny Poradniki
Karsten Kreh Karsten Kreh
Zaktualizowano

Po każdym połączeniu Safina wysyła podsumowanie na Twój adres URL webhooka. Twój CRM, workflow w n8n lub Zap tworzy z niego leada albo zadanie. Ale adres URL webhooka to tylko adres. Każdy, kto go zna, może wysłać na niego fałszywe zdarzenie „połączenie zakończone”.

Podpis rozwiązuje ten problem. Safina podpisuje każde żądanie sekretem, który znasz tylko Ty i Safina. Twój odbiornik sprawdza podpis i wie dwie rzeczy: żądanie wysłała Safina i nikt po drodze nie zmienił treści. Znacznik czasu w podpisie dodatkowo uniemożliwia ponowne wysłanie starego żądania.

Webhooki to dla Ciebie nowość? Przeczytaj najpierw Czym jest webhook?. Konfiguracja w aplikacji jest opisana w poradniku Połączenie Safina AI z CRM za pomocą webhooka.

Czego potrzebujesz

WymaganieSzczegóły
Plan SafinaPro lub wyższy
Webhook w aplikacji SafinaZ sekretem podpisu (patrz niżej)
OdbiornikTwój własny serwer, n8n, Zapier lub Make

Webhooki tworzysz i podpisujesz w aplikacji Safina. Możesz je też tworzyć przez REST API Safiny (POST /webhooks). REST API i klucze API są w wersji beta: wczesny dostęp przyznajemy na prośbę wysłaną na info@safina.ai, a klucze API wymagają planu Pro lub wyższego.

Krok 1: Włącz podpis

Podpis jest opcjonalny. Safina podpisuje żądania webhooka tylko wtedy, gdy ma on sekret podpisu. Sekret dostajesz na trzy sposoby:

  • Nowy webhook w aplikacji: W aplikacji Safina otwórz zakładkę Safina AI, potem „CRM Synchronizacja”, i dodaj webhook. Przed zapisaniem włącz Podpisuj wysyłki.
  • Istniejący webhook w aplikacji: Dotknij Utwórz sekret przy webhooku. Webhooki sprzed tej funkcji pozostają niepodpisane, dopóki tego nie zrobisz.
  • REST API (beta): Wyślij "create_signing_secret": true w POST /webhooks. Odpowiedź zawiera signing_secret.

Sekret zaczyna się od whsec_. Safina pokazuje go tylko raz. Skopiuj go i zapisz w swoim odbiorniku, tak jak hasło.

Webhook bez sekretu nadal działa i wysyła niepodpisane żądania, tak jak wcześniej. Możesz też użyć uwierzytelniania webhooka (auth_type): Safina wysyła wtedy z każdym żądaniem token bearer albo klucz API w nagłówku. To dowodzi, że nadawca zna token. Podpis dowodzi dodatkowo, że treść się nie zmieniła i że żądanie jest nowe.

Krok 2: Zrozum nagłówek

Każde podpisane żądanie ma ten nagłówek:

X-Safina-Signature: t=1794569912,v1=5f2c…e91a
  • t to czas próby dostarczenia w sekundach Unix.
  • v1 to HMAC-SHA256 tekstu <t>.<surowe body>, z Twoim sekretem podpisu jako kluczem, w zapisie szesnastkowym.

Twój odbiornik sprawdza trzy rzeczy:

  1. Czas: Odrzuć żądanie, jeśli t różni się od Twojego zegara o więcej niż 300 sekund (5 minut) w przód lub w tył.
  2. Podpis: Oblicz HMAC z surowego body żądania, dokładnie takiego, jakie przyszło, zanim sparsujesz JSON. Jeśli sparsujesz JSON i zapiszesz go ponownie, mogą zmienić się spacje albo kolejność kluczy, i podpis przestanie pasować.
  3. Porównanie: Porównaj swoją wartość z każdą wartością v1 w stałym czasie. Wystarczy jedno dopasowanie.

Dlaczego może być więcej niż jedno v1? Gdy dotkniesz Utwórz nowy sekret, stary sekret pozostaje ważny przez 24 godziny. W tym czasie nagłówek ma jedną wartość v1 dla każdego ważnego sekretu. Dzięki temu zaktualizujesz odbiornik bez utraty zdarzeń.

Każda próba dostarczenia ma nowy znacznik czasu i nowy podpis. Ponowna próba wysyła to samo event_id, więc pomijaj event_id, które już przetworzyłeś.

Krok 3: Sprawdź podpis w kodzie

Te przykłady są takie same jak w dokumentacji API (po angielsku). Przetestowaliśmy oba na prawdziwych podpisach z kodu podpisującego Safiny.

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)
    );
  });
};

W Express odczytaj body tej trasy jako surowe bajty, na przykład za pomocą express.raw({ type: "application/json" }). Wtedy req.body jest Bufferem. Parsuj JSON dopiero po sprawdzeniu:

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
    )

We Flasku surowe bajty daje request.get_data(). W FastAPI użyj await request.body().

Sprawdź podpis w narzędziach do automatyzacji

n8n

n8n sprawdza podpis za pomocą węzła Code. Jego JavaScript ma moduł crypto z Node.js: w n8n Cloud jest dostępny, a w n8n na własnym serwerze włączasz go zmienną środowiskową NODE_FUNCTION_ALLOW_BUILTIN=crypto (dokumentacja n8n).

  1. W węźle Webhook wybierz POST i włącz opcję Raw Body. n8n zachowa wtedy body jako dane binarne. Nazwę pola binarnego znajdziesz w wyniku węzła (zwykle data).
  2. Dodaj za nim węzeł Code w trybie „Run Once for All Items” z tym kodem:
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")) }];

Jeśli podpis jest błędny, workflow się zatrzymuje. Jeśli jest poprawny, kolejne węzły dostają sparsowane zdarzenie. Węzeł Crypto w n8n też potrafi obliczyć HMAC (akcja Hmac, typ SHA256, kodowanie HEX), ale najpierw musisz zbudować tekst <t>.<surowe body>, a potem porównać wartości w węźle IF. Węzeł Code robi wszystko w jednym kroku.

Zapier

W Zapier użyj triggera Webhooks by Zapier → Catch Raw Hook. W odróżnieniu od Catch Hook daje on niesparsowane body i nagłówki żądania (pomoc Zapier). Webhooks by Zapier wymaga planu Professional lub wyższego.

  1. Dodaj krok Code by Zapier (Run JavaScript). W Input Data przypisz rawBody do surowego body triggera, a signature do nagłówka X-Safina-Signature.
  2. Wklej ten kod. Biblioteka standardowa Node.js, a z nią crypto, jest dostępna w Code by Zapier (pomoc Zapier):
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. Dodaj krok Filter: kontynuuj tylko wtedy, gdy valid ma wartość true.

Sekret jest w kodzie kroku, więc widzi go każdy, kto może edytować Zap. Catch Raw Hook nie dzieli JSON-a na pola. Aby przypisywać pojedyncze pola w kolejnych krokach, sparsuj body w tym samym kroku kodu i zwróć potrzebne pola.

Make

Make może wykonać sprawdzenie za pomocą wbudowanych funkcji. Funkcja sha256(text; [encoding]; [key]) zwraca HMAC, gdy podasz jej klucz (pomoc Make). W module Custom webhook włącz JSON pass-through (dostajesz oryginalny JSON jako tekst), a w ustawieniach zaawansowanych Get request headers (pomoc Make). Następnie dodaj za webhookiem filtr z dwoma warunkami:

  • t z nagłówka różni się od formatDate(now; "X") (sekundy Unix) najwyżej o 300 sekund.
  • Nagłówek X-Safina-Signature zawiera v1=, a po nim sha256(<t>.<surowe body>; hex; <Twój sekret>).

To działa, ale filtr nie porównuje w stałym czasie, a t musisz wyciągnąć z nagłówka za pomocą split i get. W większości scenariuszy Make prostszym wyborem jest uwierzytelnianie webhooka: Make może chronić custom webhook kluczem API, którego oczekuje w nagłówku x-make-apikey (pomoc Make). W Safinie wybierz uwierzytelnianie „Klucz API”, lokalizację „Nagłówek” i nazwę nagłówka x-make-apikey, a potem wpisz ten sam klucz.

Częste błędy

  • Podpis nigdy nie pasuje: Twój framework sparsował JSON przed sprawdzeniem. Użyj surowego body.
  • Podpis pasuje, ale sprawdzenie czasu się nie udaje: Zegar Twojego serwera chodzi źle. Zsynchronizuj go przez NTP.
  • Po nowym sekrecie nic nie działa: Stary sekret jest ważny tylko 24 godziny. Zapisz nowy sekret w swoim odbiorniku.
  • To samo zdarzenie przychodzi dwa razy: To ponowna próba po przekroczeniu czasu albo błędzie. Pomiń event_id, które już przetworzyłeś.

Podsumowanie

Włącz Podpisuj wysyłki dla każdego webhooka, zapisz sekret raz i sprawdzaj X-Safina-Signature, zanim przetworzysz zdarzenie: czas, surowe body, porównanie w stałym czasie. Wtedy żadne fałszywe żądanie nie trafi do Twojego CRM. Masz pytania? Napisz do nas na info@safina.ai.

9:41

Safina obsłużyła w tym tygodniu 51 połączeń

46

Zaufane

4

Podejrzane

1

Niebezpieczne

Ostatnie 7 dni
Filter
EM
Emma Martin 67s 15:30

Chce omówić ofertę na nową kampanię i ma pytania dotyczące harmonogramu.

KN
Katarzyna Nowak 54s 14:45

Pyta o status zamówienia i termin dostawy.

TW
Tomasz Wiśniewski 34s 13:10

Umówienie spotkania w sprawie projektu na przyszły tydzień.

Nieznany 44s 11:30

Obietnica wygranej — prawdopodobnie spam.

MW
Magdalena Wójcik 10s 09:15

Reklamacja ostatniego zamówienia, prosi o oddzwonienie.

PZ
Piotr Zieliński 95s 13 gru

Chce omówić potencjalną współpracę.

AR
Anna Rutkowska 85s 13 gru

Jest Twoją koleżanką i chce porozmawiać o projekcie.

JK
Jakub Krawczyk 42s 12 gru

Pyta o dostępne terminy na przyszły tydzień.

LB
Lena Bąk 68s 12 gru

Ma pytania dotyczące faktury i prosi o wyjaśnienie.

Połączenia
Safina
Kontakty
Profil
9:41
Połączenie od Emmy Martin
12 gru
11:30
67s
+48223456789

Chce omówić ofertę na nową kampanię i ma pytania dotyczące harmonogramu.

Kluczowe punkty

  • Oddzwonić do Emmy Martin
  • Wyjaśnić pytania o harmonogram i warunki cenowe
Oddzwoń
Edytuj kontakt

Wgląd AI

Nastrój rozmówcy Bardzo dobry

Rozmówca był współpracujący i przekazał potrzebne informacje.

Pilność Niski

Rozmówca może poczekać na odpowiedź.

Audio i transkrypcja

0:16

Dzień dobry, tu Safina AI, cyfrowy asystent Piotra. W czym mogę pomóc?

Dzień dobry Safina, tu Emma Martin. Chciałam porozmawiać o ofercie i harmonogramie.

Dziękuję, Emmo. Czy przy wdrożeniu rozważają Państwo głównie pakiet Standard czy Pro?

Dokładnie. Potrzebujemy pakietu Pro i chcielibyśmy wystartować w przyszłym miesiącu, jeśli onboarding będzie możliwy w pierwszym tygodniu.

Pożegnaj się ze staromodną pocztą głosową.

Wypróbuj Safina za darmo i zacznij inteligentnie zarządzać swoimi połączeniami.

Safina Support Bot
Safina Support Bot

Cześć! 👋 Jestem asystentem wsparcia Safina. Zapytaj mnie o konfigurację, przekierowanie połączeń, plany lub Twoje konto. Uwaga: Twoje wiadomości są przetwarzane zgodnie z naszą polityką prywatności.

Twoje wiadomości są przetwarzane zgodnie z naszą polityką prywatności