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
| Wymaganie | Szczegóły |
|---|---|
| Plan Safina | Pro lub wyższy |
| Webhook w aplikacji Safina | Z sekretem podpisu (patrz niżej) |
| Odbiornik | Twó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": truewPOST /webhooks. Odpowiedź zawierasigning_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
tto czas próby dostarczenia w sekundach Unix.v1to HMAC-SHA256 tekstu<t>.<surowe body>, z Twoim sekretem podpisu jako kluczem, w zapisie szesnastkowym.
Twój odbiornik sprawdza trzy rzeczy:
- Czas: Odrzuć żądanie, jeśli
tróżni się od Twojego zegara o więcej niż 300 sekund (5 minut) w przód lub w tył. - 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ć.
- Porównanie: Porównaj swoją wartość z każdą wartością
v1w 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).
- 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). - 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.
- Dodaj krok Code by Zapier (Run JavaScript). W Input Data przypisz
rawBodydo surowego body triggera, asignaturedo nagłówkaX-Safina-Signature. - 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 };
- Dodaj krok Filter: kontynuuj tylko wtedy, gdy
validma 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:
tz nagłówka różni się odformatDate(now; "X")(sekundy Unix) najwyżej o 300 sekund.- Nagłówek
X-Safina-Signaturezawierav1=, a po nimsha256(<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.