Przejdź do głównej zawartości

Webhooki

Webhooki wypychają zdarzenia na wskazany URL, zamiast zmuszać Cię do pollingu API. Każde zdarzenie jest podpisane HMAC sekretem przypisanym do endpointu, więc możesz zweryfikować, że naprawdę pochodzi ze Stawki, zanim na nie zareagujesz.

Webhooki są dostępne w planach Hobby i Pro. W planie Free przycisk tworzenia jest wyłączony; istniejące endpointy z wcześniejszego płatnego planu są zachowane, ale wstrzymane — po przejściu z powrotem na plan płatny wznawiają się automatycznie.

  1. W panelu wejdź w Webhooki (w swojej organizacji).
  2. Utwórz webhook → wpisz URL odbiorcy i zaznacz co najmniej jedno zdarzenie.
  3. Skopiuj sekret podpisujący pokazany jednorazowo — Stawka nigdy go nie pokaże ponownie. Przechowuj go jak każdy inny sekret.
  4. Przycisk Wyślij zdarzenie testowe w panelu wystrzeliwuje syntetyczne dostarczenie, więc możesz sprawdzić swój endpoint zanim popłyną prawdziwe zdarzenia.

Zestaw zdarzeń możesz zmienić w dowolnej chwili; sekret rotujesz ze strony szczegółów endpointa. Po rotacji poprzedni sekret przestaje weryfikować podpisy natychmiast — zaktualizuj swojego odbiorcę zanim wystrzeli kolejne zdarzenie.

ZdarzenieKiedy strzelaCo jest w data
key.createdNowy klucz API utworzony w panelu.{ keyId, prefix, name, createdBy }
key.revokedKlucz unieważniony z panelu.{ keyId, prefix, name, revokedBy }
key.rotatedKlucz zrotowany (stary unieważniony, nowy utworzony jako para).{ oldKeyId, newKeyId, prefix }
quota.thresholdPierwszy raz w danym miesiącu organizacja przekracza 80% miesięcznej kwoty zapytań. Strzela raz na miesiąc per organizacja.{ planId, limit, used, percent }
quota.exceededZapytanie zostało odrzucone, bo organizacja przekroczyła miesięczną kwotę. Strzela przy każdym odrzuconym zapytaniu — traktuj jako sygnał „przestań walić”, nie jako jednorazowy alert.{ planId, limit, used, retryAfterSeconds }
subscription.upgradedPlan zmieniony na wyższy (Free → Hobby/Pro, Hobby → Pro). Free → płatny emituje upgraded.{ fromPlan, toPlan }
subscription.downgradedPlan zmieniony na niższy lub anulowany (cokolwiek → Free).{ fromPlan, toPlan }
rates.changedCodzienny cron TEDB wykrył co najmniej jeden ruch stawki VAT względem wczorajszego snapshotu.{ effectiveDate, changes[] } — ten sam kształt co endpoint /changes.

Bądź wstrzemięźliwy w subskrypcjach. quota.exceeded dla popularnego klucza może wystrzelić tysiące razy na minutę pod atakiem. Subskrybuj tylko te zdarzenia, które naprawdę potrzebuje Twój odbiornik.

Każde dostarczenie — niezależnie od zdarzenia — ma ten sam zewnętrzny kształt:

{
"id": "whd_2bX5tQ9...", // id dostarczenia; takie samo przy retry
"event": "rates.changed", // jedna z nazw zdarzeń powyżej
"orgId": "org_abc123", // id Twojej organizacji
"createdAt": "2026-05-30T07:00:23.000Z",
"livemode": true, // zawsze true na produkcji
"data": { /* zawartość zależna od zdarzenia */ }
}

id to id wiersza dostarczenia, stabilny przy retry tego samego dostarczenia. Udany odbiornik powinien deduplikować po tym polu — jeśli już przetworzyłeś whd_2bX5tQ9... i przyszło retry, potwierdź ACK-iem bez ponownej akcji.

Stawka wysyła nagłówek Stawka-Signature przy każdym zapytaniu:

Stawka-Signature: t=1748583623,v1=8b2c91a7e9d4f5c1...
  • t — UNIX-sekundy z momentu podpisania.
  • v1 — hex SHA-256 HMAC z ${t}.${rawBody} (lower case), z kluczem-sekretem Twojego endpointa. Prefiks ${t}. to obrona przed replayem: atakujący, który przechwyci jeden podpisany body, nie może go wysyłać w nieskończoność.

Format wzorowany na webhookach Stripe’a — odbiorcy znający ten wzorzec mają o jedną nową rzecz mniej do nauki. v1 to znacznik wersji; gdybyśmy musieli zmienić algorytm, przez okno wycofania wysyłaliśmy oba: v1 i v2.

  1. Odczytaj surowy body PRZED parsowaniem JSON. Białe znaki, kolejność kluczy i formatowanie liczb wpływają na HMAC. Nie serializuj ponownie z obiektu — podpisuj dokładnie te bajty, które otrzymałeś.
  2. Odrzuć wszystko z różnicą ponad ±5 minut. Porównaj t z zegarem serwera; poza tym oknem zwróć 400.
  3. HMAC-SHA256 z ${t}.${rawBody} Twoim sekretem endpointa. Porównaj w stałym czasie z polem v1 (hex).
import crypto from "node:crypto";
function verifyStawkaSignature(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=")),
);
const t = Number.parseInt(parts.t, 10);
if (!Number.isFinite(t)) return false;
if (Math.abs(Date.now() / 1000 - t) > 5 * 60) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(parts.v1, "hex"),
);
}

Wywołanie crypto.timingSafeEqual ma znaczenie — === na stringach hex wycieka informacje o czasie, co pozwala atakującemu odzyskać sekret bajt po bajcie. Ta sama lekcja w każdym języku: porównuj w stałym czasie.

Wysyłamy POST i oczekujemy 2xx jako ACK. Każda inna odpowiedź — albo jej brak w ciągu 10 sekund — liczy się jako porażka i ponownie wstawiamy do kolejki.

Harmonogram, indeksowany numerem nieudanej próby:

PróbaOdstęp przed kolejną
130 s
21 min
32 min
44 min
58 min
616 min
732 min
81 h
92 h
104 h
11— (oznaczone jako dead, brak dalszych retry)

Każde opóźnienie ma jitter ±10%, żeby rozłożyć retry między odbiorców, którzy psują się jednocześnie (np. gdy popularny host webhooków ma awarię). Łączny budżet to 11 prób.

Po 5 kolejnych nieudanych próbach endpoint jest automatycznie wstrzymany. Już wstawione dostarczenia kończą swój budżet retry; nowe zdarzenia nie wchodzą do kolejki, dopóki nie naprawisz odbiornika i nie włączysz endpointa ponownie z panelu. Pojedyncze udane dostarczenie zeruje licznik kolejnych porażek.

  • At-least-once. Wstrzymanie sieci w połowie retry może spowodować duplikat dostarczenia tego samego id. Zawsze deduplikuj.
  • Kolejność best-effort. W obrębie jednego endpointa dostarczenia z reguły przychodzą w kolejności emisji, ale retry mogą to przetasować. Nie zakładaj, że key.created przyjdzie przed key.revoked dla tego samego klucza — jeśli kolejność istotna, odczytaj stan z API.
  • Porażki są obserwowalne. Każde dostarczenie jest logowane ze statusem, kodem HTTP i ewentualnym błędem sieciowym. Strona szczegółów webhooka pokazuje ostatnie dostarczenia; psujące się wiersze są widoczne inline, więc debugujesz bez grepowania logów.

Żeby zdarzenie rates.changed dotarło do Twojego odbiornika, wszystkie trzy warunki muszą być spełnione:

  1. Twoja organizacja jest w planie Hobby lub Pro (sprawdzane ponownie w chwili strzału, więc downgrade na sekundę przed wysyłką zatrzymuje dostarczenie bez ruszania endpointa).
  2. Endpoint jest włączony w panelu.
  3. rates.changed jest w zestawie subskrybowanych zdarzeń endpointa.

Ta sama bramka działa dla każdego zdarzenia. Ponowny sprawdzian planu to kontrakt „retain but pause” — Twoje endpointy przeżywają downgrade i wznawiają się w chwili powrotu do planu płatnego; nie trzeba ich konfigurować od nowa.

  • Referencja payloadu zdarzeń — kształt data per zdarzenie jest udokumentowany w endpointach, gdzie dane zdarzenia są najbardziej istotne. Dla rates.changed zobacz /changes — ta sama tablica changes.
  • Konfiguracja krok po kroku i edycja — w panelu pod Webhooki.
  • Rotacja sekretu — strona szczegółów webhooka w panelu.