Przejdź do głównej zawartości

Webhooki

Webhook to sposób, w jaki OctaForms powiadamia Twój serwer o nowym zgłoszeniu. Po wysłaniu formularza OctaForms robi POST na skonfigurowany adres URL z danymi leada w formacie JSON. Endpoint i sekret podpisu ustawiasz w panelu, dla każdego formularza osobno.

POST /twoj-endpoint HTTP/1.1
Content-Type: application/json
X-OctaForms-Signature: t=1752573000, v1=3f9a...c1
{
"id": "018f5c2e-...",
"formId": "7c2e1a9d-...",
"overviewUrl": "https://panel.octaforms.pl/leads/018f5c2e-...",
"created": "2026-07-15T10:30:00+0000",
"fields": {
"name": "Jan Kowalski",
"email": "jan@example.com",
"phone": "+48500600700",
"zgoda": true
},
"meta": {
"form-url": "https://twoja-strona.pl/kontakt",
"referrer": "https://www.google.com/"
}
}
Pole Typ Znaczenie
id string Identyfikator zgłoszenia (leada). Użyj go do deduplikacji po swojej stronie.
formId string Identyfikator formularza, z którego przyszło zgłoszenie.
overviewUrl string (URL) Link do podglądu zgłoszenia w panelu OctaForms.
created string Czas utworzenia zgłoszenia w formacie ISO 8601, Y-m-d\TH:i:sO (np. 2026-07-15T10:30:00+0000, offset UTC bez dwukropka).
fields obiekt Wartości pól formularza, kluczowane nazwą pola. Pola typu checkbox są typu boolean.
meta obiekt Kontekst dołączony przez widget na stronie. Przekazywany bez zmian. Traktuj klucze jako opcjonalne.

meta to otwarty worek, klucze bywają obecne zależnie od konfiguracji na stronie. Najczęstsze:

Klucz Typ Znaczenie
form-url string (URL) Pełny adres strony, na której wysłano formularz.
referrer string Adres, z którego trafił użytkownik (surowy document.referrer, pusty gdy brak).
context string Tylko widget kontaktowy („słuchawka“): wartość contact-widget.
tab string Tylko widget kontaktowy: callback lub message.

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

X-OctaForms-Signature: t=<unix-timestamp>, v1=<hmac_sha256(sekret, t + "." + rawBody)>
  • t to znacznik czasu (sekundy Unix) z momentu wysłania.
  • v1 to HMAC SHA-256 liczony z sekretu i wiadomości t + "." + rawBody, w zapisie hex (małe litery).
  • rawBody to surowe bajty ciała żądania. Licz HMAC z nieprzetworzonego body, nie z ponownie zserializowanego JSON-a, inaczej podpis się nie zgodzi.

Podczas rotacji sekretu nagłówek może nieść dwa człony v1= (nowy i poprzedni sekret). Zaakceptuj żądanie, jeśli którykolwiek się zgadza.

  1. Odczytaj surowe body żądania.
  2. Wyciągnij t z nagłówka (wzorzec t=(\d+)).
  3. Odrzuć żądanie, jeśli abs(now - t) > 300 sekund (ochrona przed replay, tolerancja ±5 min).
  4. Policz expected = HMAC_SHA256(sekret, t + "." + rawBody) jako hex.
  5. Wyciągnij wszystkie wartości v1=([a-f0-9]+) z nagłówka.
  6. Porównaj expected z każdym v1 w sposób odporny na timing (hash_equals / timingSafeEqual). Zaakceptuj, jeśli którykolwiek pasuje.
  7. Przy niezgodności odpowiedz 400.
import crypto from 'node:crypto';
const SECRET = process.env.OCTAFORMS_WEBHOOK_SECRET;
// ważne: potrzebujemy surowego body, więc bez globalnego express.json()
app.post('/webhooks/octaforms', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-OctaForms-Signature') || '';
const rawBody = req.body; // Buffer z surowymi bajtami
const t = (header.match(/t=(\d+)/) || [])[1];
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) {
return res.status(400).end();
}
const expected = crypto
.createHmac('sha256', SECRET)
.update(`${t}.${rawBody.toString('utf8')}`)
.digest('hex');
const signatures = [...header.matchAll(/v1=([a-f0-9]+)/g)].map((m) => m[1]);
const ok = signatures.some((sig) => {
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(sig, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
});
if (!ok) return res.status(400).end();
const lead = JSON.parse(rawBody.toString('utf8'));
// ...obsłuż zgłoszenie...
res.status(200).end();
});

Sekret znajdziesz i zrotujesz w panelu OctaForms, przy konfiguracji webhooka. Rotacja nie unieważnia od razu starego sekretu, przez chwilę oba są ważne (stąd dwa człony v1=).

  • Timeouty: połączenie 5 s, całe żądanie 10 s. Endpoint musi odpowiedzieć w tym czasie.
  • Sukces: dowolna odpowiedź 2xx. Każdy inny status (4xx/5xx) albo timeout to porażka.
  • Ponawianie: do 3 prób po pierwszej dostawie, z narastającym odstępem 1 s, 2 s, 4 s.
  • Deduplikacja: dostawa jest „co najmniej raz“. Przy ponowieniu OctaForms wysyła ten sam payload z tym samym id. Deduplikuj po polu id.