Webhooki — przebieg integracji¶
Webhooki to jedyny kanał, którym informujemy partnera o zdarzeniach powstałych po naszej stronie. Wszystko pozostałe w API jest odpowiedzią na zapytanie partnera.
Po co są¶
Część zdarzeń nie wynika z działania użytkownika: przypomnienie o zabiegu wystawia nasz automat, odpowiedź weterynarza na czacie przychodzi w dowolnym momencie, ankieta kontrolna po wizycie powstaje dwa dni po niej. W aplikacji PetAssist sygnalizują je mail, powiadomienie w aplikacji i push — użytkownikom integracji nie wysyłamy żadnego z nich, zgodnie z ustaleniem, że komunikacja z użytkownikiem należy do partnera.
Webhook zastępuje więc te trzy kanały. Bez jego obsługi część funkcji pozostanie niewidoczna dla użytkownika, dopóki sam nie otworzy właściwego ekranu.
Konfiguracja¶
Po stronie partnera potrzebny jest jeden adres HTTPS przyjmujący POST. Adres i sekret
ustawiamy my, przy integracji — nie ma endpointu do samodzielnej zmiany.
Zdarzenia¶
Ładunek niesie wyłącznie identyfikatory i czas. Nie wysyłamy tytułów ani treści powiadomień: brzmienie komunikatu należy do partnera, tak samo jak decyzja, czy w ogóle je pokazać.
| Zdarzenie | Kiedy | data |
|---|---|---|
chat.message.received |
odpowiedź weterynarza (albo wiadomość automatyczna) w konwersacji użytkownika | conversationId |
pet.cyclic_treatment.reminder |
termin czynności pielęgnacyjnej | petCyclicTreatmentId, notificationId |
pet.external_appointment.reminder |
termin zaplanowanej wizyty zewnętrznej | externalAppointmentId, petId |
pet.treatment.reminder |
termin badania, szczepienia, odrobaczania lub profilaktyki | externalAppointmentId, petId, cyclicTreatmentId |
visit.monitoring.created |
powstała ankieta kontrolna po wizycie u weterynarza z naszej sieci | monitoringId, visitId, petId, count |
Zdarzenie czatu dotyczy wyłącznie wiadomości przychodzących — własna wiadomość
użytkownika, wysłana przez POST /chat/{conversationId}/messages, żadnego webhooka nie
wywołuje.
Leki nie mają dziś własnego zdarzenia — przypomnienia o nich widać na osi czasu.
Ostatnie zdarzenie dotyczy wyłącznie wizyt zakładanych przez naszych weterynarzy; wizyty wpisywane przez właściciela żadnej ankiety nie generują. Skąd biorą się jedne i drugie, opisuje dokumentacja wizyt.
Co zrobić z każdym z nich¶
| Zdarzenie | Dokąd po szczegóły |
|---|---|
chat.message.received |
GET /chat/{conversationId}/messages?take=30 |
pet.cyclic_treatment.reminder |
GET /pets/cyclic-treatment-reminders/{reminderId} |
pet.external_appointment.reminder |
GET /pets/{petId}/external-appointments/{externalAppointmentId} |
pet.treatment.reminder |
jak wyżej — zabieg przychodzi razem z wizytą |
visit.monitoring.created |
GET /med-visit/monitoring/{monitoringId} |
Zwróć uwagę, że przy pielęgnacji notificationId to identyfikator przypomnienia —
i to jego wstawia się w ścieżkę jako {reminderId}. Identyfikator samej czynności
przychodzi obok, jako petCyclicTreatmentId, i służy do jej edycji albo oznaczenia
wykonania. Ten sam podział opisuje dokumentacja pielęgnacji.
Format żądania¶
POST https://… (adres partnera)
content-type: application/json
x-webhook-event: pet.treatment.reminder
x-webhook-id: 8f14e45f-ce15-4f3a-9a2b-1d5f2b7c0e11
x-webhook-signature: 9a1c… (HMAC-SHA256, hex)
{ "eventId": "8f14e45f-ce15-4f3a-9a2b-1d5f2b7c0e11",
"type": "pet.treatment.reminder",
"userId": "…",
"occurredAt": "2026-09-10T14:30:00.000Z",
"data": { "externalAppointmentId": "…", "petId": "…", "cyclicTreatmentId": "…" } }
userId to identyfikator użytkownika w tej integracji — ten sam, który partner wysyła
w nagłówku x-user-id. Po nim rozstrzyga się, kogo dotyczy zdarzenie.
Weryfikacja podpisu¶
x-webhook-signature to HMAC-SHA256 z surowego ciała żądania, kluczem jest sekret
integracji, wynik zapisany szesnastkowo. Liczy się bajty ciała przed parsowaniem
JSON — ponowne serializowanie sparsowanego obiektu da inny podpis.
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(rawBody)
.digest('hex');
const ok = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(req.headers['x-webhook-signature']),
);
Porównanie warto wykonać funkcją odporną na pomiar czasu, jak wyżej.
Odpowiedź i ponawianie¶
Oczekujemy statusu 2xx. Wszystko inne, tak samo jak brak odpowiedzi w ciągu 10 sekund, uznajemy za niepowodzenie i ponawiamy:
| Próba | Odstęp od poprzedniej |
|---|---|
| 2 | 1 minuta |
| 3 | 5 minut |
| 4 | 15 minut |
| 5 | 1 godzina |
| 6 | 3 godziny |
| 7 | 8 godzin |
Po siedmiu nieudanych próbach doręczenie oznaczamy jako nieudane i więcej nie ponawiamy. Tak samo kończą zdarzenia czekające w kolejce, jeśli w międzyczasie webhooki zostaną dla integracji wyłączone — nie są wtedy doręczane ani przechowywane do później. Kolejka jest obsługiwana co minutę, więc pierwsze doręczenie następuje do minuty od zdarzenia.
Idempotencja¶
eventId (równe nagłówkowi x-webhook-id) jest stałe dla wszystkich ponowień tego
samego zdarzenia. Należy go zapisywać i pomijać ładunki już przetworzone — inaczej
przy powtórce powstaną zduplikowane powiadomienia u użytkownika.
Odpowiadaj szybko¶
Doręczenie liczy się jako nieudane po 10 sekundach, a odpowiadanie po tym czasie
powoduje ponowienia. Zalecamy przyjąć ładunek, odpowiedzieć 200 i dopiero potem
odpytywać nasze API o szczegóły.
Najczęstsze przyczyny błędów¶
| Objaw | Przyczyna |
|---|---|
| Podpis się nie zgadza | Policzono go z ponownie zserializowanego JSON-a zamiast z surowego ciała |
Brak nagłówka x-webhook-signature |
Integracja nie ma ustawionego sekretu — prosimy o kontakt |
| Zdarzenia przychodzą wielokrotnie | Odpowiedź inna niż 2xx albo wolniejsza niż 10 s; sprawdź eventId przed przetworzeniem |
| Zduplikowane powiadomienia u użytkownika | Brak sprawdzania eventId |
| Nic nie przychodzi | Adres nieustawiony albo webhooki wyłączone po naszej stronie |
| Przychodzi zdarzenie czatu, choć weterynarz nic nie napisał | To wiadomość automatyczna — z punktu widzenia użytkownika jest zwykłą wiadomością w rozmowie |