Webhooki: przekazywanie zdarzeń do własnych systemów
Po lekturze tego artykułu masz skonfigurowany webhook, który przy wybranych przez Ciebie zdarzeniach wywołuje Twój własny adres — z kluczem podpisu, dostawą próbną, dziennikiem i automatycznym ponawianiem w razie błędów.
Webhook odwraca kierunek: to nie Twój system pyta Univents o zmiany, tylko Univents sam zgłasza się do Twojego systemu, gdy coś się wydarzy — wpłynie zapytanie, oferta zostanie zaakceptowana albo zostanie wystawiona faktura. Dzięki temu własne programy, skrypty lub usługi automatyzujące podpinają się do Twojego workspace, a nikt nie musi regularnie sprawdzać, czy coś się zmieniło.
Konfigurujesz to w samym workspace, w sekcji Integracje → Dla deweloperów → Webhooki. Konfiguracja, klucze i dziennik są widoczne tylko dla roli Właściciel — to tam widać, jakie adresy wywołuje Univents.
Kiedy warto użyć webhooka
Użyj webhooka, gdy Twój własny system ma zareagować natychmiast: odzwierciedlić zapytanie we własnym CRM, uruchomić wysyłkę w systemie magazynowym, dociągnąć fakturę do księgowości, odświeżyć tablicę informacyjną. Do wszystkiego, co ma zostać wewnątrz Univents, lepsze są Automatyzacje.
Dodawanie endpointu
- Kliknij w lewym menu głównym Integracje, potem po lewej kategorię Dla deweloperów i otwórz kartę Webhooki.
- Kliknij w prawym górnym rogu Dodaj webhook.
- W polu Nazwa wpisz coś, co później łatwo rozpoznasz, na przykład
CRM-Sync. - W polu Adres wpisz pełny adres odbiorcy zaczynający się od
https://. Tylkohttps— niezaszyfrowane adresy są odrzucane. Univents odrzuca też adresy z Twojej sieci lokalnej, zlocalhostlub z sieci wewnętrznej. - Zaznacz Zdarzenia, które ten odbiorca ma otrzymywać.
- Zapisz. Univents wyświetli teraz klucz podpisu — Jednorazowo.
Klucz podpisu
Klucz zaczyna się od whsec_. Jest wyświetlany dokładnie raz — przy dodawaniu i przy ponownym generowaniu. Potem widzisz już tylko jego początek, dzięki czemu w workspace da się ustalić, który klucz jest zapisany, a sama wartość nie leży na wierzchu. Skopiuj go więc od razu do swojego systemu (zmienna środowiskowa, magazyn sekretów).
Przyciskiem Wygeneruj nowy klucz podpisu możesz w każdej chwili dostać nowy klucz. Stary natychmiast traci ważność — podmień go po swojej stronie w tym samym momencie, w przeciwnym razie Twój odbiorca będzie odrzucał przychodzące wiadomości.
Weryfikacja podpisu
Do każdej dostawy Univents dołącza trzy nagłówki:
| Nagłówek | Treść |
|---|---|
X-Univents-Signature | t=<Zeitstempel>,v1=<Signatur> |
X-Univents-Event | klucz wyzwalacza, na przykład inquiry_created |
X-Univents-Delivery-Id | identyfikator tej dostawy |
Podpis to HMAC-SHA256 z tekstu ${t}.${Body} — czyli znacznik czasu, kropka i surowe body, bajt w bajt tak, jak dociera. Ważne: weryfikuj surowe body, a nie zserializowane na nowo. JSON.parse, a następnie JSON.stringify zmienia kolejność pól i znaki escape, przez co weryfikacja się nie powiedzie, mimo że wiadomość jest w porządku.
Tak wygląda weryfikacja w Node:
const crypto = require('node:crypto');
const express = require('express');
const app = express();
// Roher Body — NICHT express.json() davor.
app.post('/webhooks/dein-system', express.raw({ type: '*/*' }), (req, res) => {
const header = req.get('X-Univents-Signature') ?? '';
const parts = Object.fromEntries(
header.split(',').map((piece) => piece.trim().split('=')),
);
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(`${parts.t}.${req.body.toString('utf8')}`)
.digest('hex');
const received = parts.v1 ?? '';
// Länge zuerst prüfen: timingSafeEqual wirft bei ungleicher Länge.
const ok =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!ok) return res.status(400).send('signature mismatch');
// Der Zeitstempel liegt unter der Signatur — so kann niemand eine alte
// Meldung erneut einspielen.
const ageSeconds = Math.abs(Date.now() / 1000 - Number(parts.t));
if (ageSeconds > 300) return res.status(400).send('stale');
const event = JSON.parse(req.body.toString('utf8'));
console.log(req.get('X-Univents-Event'), event);
res.sendStatus(200);
});
Zwróć 200 (lub inną odpowiedź z zakresu 2xx), gdy tylko przyjmiesz wiadomość. Każda inna odpowiedź albo odpowiedź, która trwa dłużej niż dziesięć sekund, jest traktowana jako nieudana próba.
Dostawy: co się dzieje w razie błędów
Dostawa następuje co najmniej raz — nie dokładnie raz. Przerwane połączenie po drodze może sprawić, że to samo zdarzenie dotrze do Ciebie drugi raz. Zbuduj więc swojego odbiorcę tak, aby dwukrotne wywołanie niczego nie psuło: zanim cokolwiek utworzysz, sprawdź X-Univents-Delivery-Id względem ostatnio przetworzonych identyfikatorów.
Jeśli Twój odbiorca nie odpowiada albo odpowiada błędem, Univents ponawia próbę. W sumie przewidziano osiem prób ze wzrastającymi odstępami:
| Próba | Odstęp od poprzedniej |
|---|---|
| 1 | natychmiast |
| 2 | 1 minuta |
| 3 | 5 minut |
| 4 | 30 minut |
| 5 | 2 godziny |
| 6 | 6 godzin |
| 7 | 12 godzin |
| 8 | 24 godziny |
Potem wiadomość uznawana jest za ostatecznie nieudaną i nie jest już ponawiana. Jeśli Twój system ma planowaną przerwę serwisową, masz w ten sposób prawie dwa dni, żeby znów być osiągalnym.
Dostawa próbna
Przyciskiem Test wysyłasz wiadomość do endpointu bez czekania na prawdziwe zdarzenie. W ten sposób po konfiguracji sprawdzisz, czy adres, weryfikacja podpisu i droga odpowiedzi działają poprawnie. Dostawa próbna trafia następnie do dziennika, żebyś nie pomylił(a) jej z prawdziwą wiadomością.
Endpoint ze statusem Wstrzymana nie otrzymuje dostawy próbnej — najpierw go aktywuj. A ponieważ dostawa próbna to jedyny sposób, w jaki możesz sprawić, by nasz serwer wywołał wskazany przez Ciebie adres, obowiązuje limit dziesięciu dostaw próbnych na workspace na godzinę. Po jego wyczerpaniu interfejs poinformuje Cię, że masz spróbować ponownie później.
Dziennik i ponowne dostarczanie
Pod każdym endpointem widać 50 ostatnich dostaw. W każdym wierszu zobaczysz:
- stan — oczekuje, w toku, dostarczona lub ostatecznie nieudana,
- zdarzenie,
- liczbę prób (na przykład
3/8), - status odpowiedzi Twojego odbiorcy.
Przez Pokaż zawartość zobaczysz wysłaną wiadomość, a przez Szczegóły ostatni błąd i termin następnej próby. Odpowiedź Twojego odbiorcy Univents zachowuje w skróconej formie jako materiał diagnostyczny.
Dostarcz ponownie wysyła jeszcze raz dostarczoną lub ostatecznie nieudaną wiadomość — to przydatne, gdy Twój system był niedostępny tylko przez chwilę. Dopóki dostawa jest w toku albo wciąż oczekuje, przycisk jest zablokowany, aby nie powstały dwa równoległe wywołania.
Wstrzymywanie i usuwanie
Przełącznik przy endpoincie ustawia status Wstrzymana: Univents od razu przestaje cokolwiek dostarczać, ale konfiguracja pozostaje — idealne rozwiązanie, gdy system jest w przebudowie. Univents odczytuje stan przy każdej dostawie na świeżo, więc wstrzymanie działa od razu, a nie dopiero przy następnym zdarzeniu.
Usuń kasuje endpoint wraz z jego dziennikiem. Wcześniej Univents prosi o potwierdzenie.
Dostępne zdarzenia
Do wyboru masz te same zdarzenia, które znają Automatyzacje. Lista w oknie rośnie wraz z produktem: gdy tylko Univents zacznie zgłaszać zdarzenie w nowym miejscu, będzie ono dostępne także dla webhooków. Zdarzenie bez zaznaczenia nie jest dostarczane.
Co zawiera wiadomość
Każda wiadomość ma tę samą kopertę:
{
"id": "48213",
"type": "inquiry_created",
"created_at": "2026-09-28T10:12:33.000Z",
"data": { "…": "…" }
}
id— identyfikator tej dostawy, taki sam jak w nagłówkuX-Univents-Delivery-Id. Użyj go jako klucza idempotentności.type— klucz wyzwalacza (inquiry_created,event_confirmed, …).created_at— kiedy powstała koperta, a nie kiedy zaszło zdarzenie.data— treść merytoryczna, dla każdego zdarzenia według stałej listy.
W data znajduje się wyłącznie to, co jest na tej liście. Pola wewnętrzne i wszystko, co mogłoby zawierać dane dostępowe lub tekst dowolny, jest pomijane — między innymi legacy_*, stripe_* (identyfikatory płatności), *token*, *secret*, *_hash*, pdf_url, pola e-mail i telefonu, notatki/opisy oraz wewnętrzne przełączniki (is_*, created_by, deleted_at). Pole bez wartości jest pomijane, a nie wysyłane jako null. Nowe kolumny w naszych tabelach nie pojawiają się tu same z siebie; dodajemy je świadomie, pojedynczo.
Każda wersja data zawiera entity_type i entity_id — dzięki temu przypiszesz wiadomość nawet wtedy, gdy zdarzenie nie ma własnego rekordu.
Zapytania
| Zdarzenie | Pola w data |
|---|---|
inquiry_created | id, event_id, contact_id, location_id, booking_page_id, booking_request_state, event_name, event_date, event_end_date, guest_count, estimated_value, account_id, created_at, updated_at |
quote_accepted | pola z inquiry_created oraz pola oferty (zob. Dokumenty finansowe) |
Wydarzenia
| Zdarzenie | Pola w data |
|---|---|
event_confirmed, event_cancelled | id, name, guest_count, budget, start_date, end_date, date_is_confirmed, language, event_state, location_id, room_id, contact_id, account_id, created_at, updated_at |
event_t_minus | jak wyżej, dodatkowo days_before, weekday |
event_t_plus | jak wyżej, dodatkowo days_after |
Dokumenty finansowe
Dotyczy invoice_overdue, invoice_paid, quote_sent, quote_expired i quote_rejected; w przypadku quote_rejected dodatkowo source.
id, event_id, contact_id, doc_type, document_number, title, version, quote_state, invoice_state, issue_date, due_date, sent_date, accepted_date, rejected_date, expires_at, net_total, gross_total, tax_total, discount_total, deposit_amount, currency, linked_order_id, prior_version_id, account_id, created_at, updated_at
Pozostałe zdarzenia
| Zdarzenie | Pola w data |
|---|---|
payment_received | id, booking_request_id, event_id, contact_id, booking_order_state, total_net, total_gross, total_tax, discount_total, currency, payment_method, paid_at, account_id, created_at, updated_at |
contract_signed | id, event_id, contact_id, title, contract_doc_type, contract_state, version, sent_at, expires_at, has_signed_pdf, account_id, created_at, updated_at |
email_received | contact_id, received_at, has_attachments, has_contact_match — bez treści e-maila, bez tematu |
schedule_monthly | day_of_month, target_date, period_start, period_end, period_label, account_id |
Dostawa próbna używa tej samej koperty z type: "webhook_test" i data: { test, endpoint_id, sent_at }.
Zdarzenie, dla którego nie ma tu listy, przychodzi z pustym data: lepsza widoczna, pusta koperta niż surowy rekord.
Ograniczenia
- Tylko https, bez przekierowań: jeśli Twój serwer odpowie przekierowaniem, jest to traktowane jako nieudana próba. Wpisz adres docelowy.
- Dziesięć sekund na każdą próbę. Cięższe operacje należy wykonywać już po udzieleniu odpowiedzi: przyjmij wiadomość, odpowiedz kodem 200 i przetwórz ją potem.
- Treść wiadomości opisuje rekord, który wywołał zdarzenie — traktuj ją jak dane klientów Twojego workspace i nie zapisuj jej dalej w logach bez kontroli.