Estalo wysyła metodą POST krótką, podpisaną wiadomość JSON na Twój adres HTTPS, gdy w biurze zdarzy się jedna z tych rzeczy:
| Zdarzenie | Kiedy |
|---|---|
lead.created | zapisano nowe zapytanie |
lead.status_changed | zmienił się status zapytania (ponowny zapis tej samej wartości się nie liczy) |
client.created | zapisano nowego klienta |
deal.stage_changed | transakcja przeszła do innego etapu |
task.completed | zadanie zmieniło status na done |
event.created | zapisano wydarzenie w kalendarzu |
lead.updated | zmieniło się cokolwiek w zapytaniu, także status (zmiana statusu wysyła to zdarzenie i lead.status_changed) |
client.updated | zmieniło się cokolwiek w kliencie |
deal.created | zapisano nową transakcję |
task.created | zapisano nowe zadanie |
property.created | zapisano nową ofertę |
property.updated | zmieniło się cokolwiek w ofercie, także przeniesienie do kosza |
Zdarzenie *.updated powstaje tylko wtedy, gdy zmieniła się zapisana wartość, a nie przy ponownym zapisie rekordu bez zmian. Dostarczanie działa w trybie „co najmniej raz”: usuwaj duplikaty po id zdarzenia, a aktualny stan rekordu pobieraj przez API (data.resource może mieć wartość lead, client, deal, task, event albo property).
Właściciel dodaje adresy w Ustawieniach › API i webhooki i dla każdego adresu wybiera zdarzenia. Biuro może mieć do 10 adresów.
Wiadomość
{
"id": "6f1c2b9e-0000-4000-8000-000000000000",
"type": "lead.created",
"schema_version": 1,
"sequence": 18342,
"created_at": "2026-10-09T10:15:02.114+00:00",
"organization_id": "00000000-0000-4000-8000-000000000000",
"data": { "resource": "lead", "id": "a5d0…", "version": "2026-10-09T10:15:02.114+00:00" }
}
Tylko identyfikatory. W webhookach nie podróżują nazwiska, numery telefonów ani kwoty; aktualny rekord pobierasz kluczem API (GET /api/v1/leads/{id}). Tam też egzekwowana jest widoczność, więc rekord, którego klucz nie widzi, zwraca 404.
data.id jest liczbą dla klientów i wydarzeń w kalendarzu, a ciągiem UUID dla pozostałych zasobów. sequence rośnie w całym Estalo; ma luki i nadaje się tylko do porządkowania zdarzeń, które już odebrałeś. data.version to updated_at rekordu w chwili zmiany.
Zdarzenia zapisują się w tej samej transakcji bazy danych co sama zmiana. Jeśli zmiana zostanie wycofana, zdarzenia nie ma; jeśli zostanie zatwierdzona, zdarzenie istnieje.
Nagłówki
| Nagłówek | |
|---|---|
X-Estalo-Event-Id | to samo co id w treści; po nim usuwaj duplikaty |
X-Estalo-Event-Type | to samo co type |
X-Estalo-Delivery-Id | jeden na parę (zdarzenie, adres); stały przy ponowieniach |
X-Estalo-Delivery-Attempt | 1, 2, … |
X-Estalo-Timestamp | czas uniksowy w sekundach, gdy ta próba została podpisana |
X-Estalo-Signature | v1=<hex HMAC-SHA256> |
Content-Type | application/json |
User-Agent | Estalo-Webhooks/1 |
Weryfikacja podpisu
Podpis to HMAC-SHA256 kluczem tajnym adresu, liczony z ciągu <X-Estalo-Timestamp>.<surowa treść żądania>. Użyj dokładnie tych bajtów, które przyszły: parsowanie i ponowna serializacja JSON zmienia je i weryfikacja się nie uda. Odrzucaj znaczniki czasu różniące się od Twojego zegara o więcej niż pięć minut, porównuj w stałym czasie i przyjmuj żądanie, jeśli pasuje którakolwiek wartość v1= z nagłówka (w przyszłości przy rotacji sekretów może ich być więcej).
Node.js:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyEstalo(secret, rawBody, timestamp, signatureHeader) {
if (!/^\d+$/.test(timestamp ?? "")) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = Buffer.from("v1=" + createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex"));
return (signatureHeader ?? "").split(",").some((part) => {
const candidate = Buffer.from(part.trim());
return candidate.length === expected.length && timingSafeEqual(candidate, expected);
});
}
Python:
import hashlib, hmac, time
def verify_estalo(secret: str, raw_body: bytes, timestamp: str, signature_header: str) -> bool:
if not timestamp or not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return False
mac = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(part.strip(), "v1=" + mac) for part in (signature_header or "").split(","))
Sekret zaczyna się od whsec_. Estalo pokazuje go raz: przy dodaniu adresu albo po kliknięciu „Nowy sekret”; przechowuj go w menedżerze sekretów. Po wygenerowaniu nowego sekretu wiadomości są podpisywane już tylko nowym.
Dostarczanie: co najmniej raz
Odpowiedz dowolnym kodem 2xx w ciągu 10 sekund, a właściwą pracę wykonaj później. Te 10 sekund obejmuje razem połączenie, uzgadnianie TLS i Twoją odpowiedź; treść odpowiedzi jest ignorowana. Wszystko inne to nieudana próba:
3xx: przekierowania nie są wykonywane;410 Gone: dostarczanie kończy się od razu, bez ponowień;- inne
4xx/5xx, przekroczenie czasu i błędy połączenia lub TLS: ponowienie po 1 min, 5 min, 30 min, 2 h, 6 h, 12 h i 24 h. Każde ponowienie ma ten samX-Estalo-Delivery-Idi wyższyX-Estalo-Delivery-Attempt. Po ósmej nieudanej próbie dostawa jest oznaczana jako nieudana i trafia do dziennika dostaw, z którego właściciel może wysłać ją ponownie.
Dostawa może zostać ponowiona, choć Twój serwer już ją przetworzył (na przykład Twoje 200 zginęło w drodze powrotnej), więc to samo zdarzenie może przyjść więcej niż raz. Zapisuj X-Estalo-Event-Id i ignoruj powtórki. Kolejność też nie jest gwarantowana; jeśli ma znaczenie, korzystaj z sequence albo data.version.
Dziesięć kolejnych dostaw zakończonych niepowodzeniem (wyczerpane ponowienia albo 410) wyłącza adres („Wyłączony po powtarzających się błędach”); każda udana dostawa w międzyczasie zeruje licznik, a właściciel może włączyć adres z powrotem w ustawieniach. Zdarzenia starsze niż 72 godziny, których nigdy nie próbowano wysłać (na przykład gdy webhooki były wyłączone na serwerze), nie są wysyłane z opóźnieniem.
Zdarzenia i ich dostawy są przechowywane przez 30 dni, potem usuwane. Dziennik dostaw obejmuje to okno, a nieudaną dostawę można wysłać ponownie tylko, dopóki w nim jest.
Co wstrzymuje wysyłkę
Każda próba sprawdza w chwili wysyłki, czy:
- adres jest nadal aktywny (jego usunięcie anuluje wszystko, co na niego czekało);
- jego twórca jest nadal aktywnym członkiem biura z aktywnym kontem;
- biuro ma aktywny plan;
- rekord jest widoczny dla twórcy: właściciel, kierownik i dyrektor widzą całe biuro, pozostałe role tylko rekordy przypisane do siebie.
Wiadomość, która nie spełnia któregoś warunku, nie jest wysyłana i pojawia się w dzienniku jako „Nie wysłano”. Jeśli biuro zostanie wyłączone z bety API, oczekujące wiadomości są oznaczane jako nieudane.
Wymagania dla adresu
https://z publiczną nazwą DNS i domyślnym portem 443. Adresy IP,localhost, nazwy jednoczłonowe i wewnętrzne sufiksy (.local,.internal,.lan,.home.arpai podobne) są odrzucane.- Każdy adres, na który rozwiązuje się nazwa, musi być publiczny. Zakresy prywatne, loopback, link-local, CGNAT, metadane chmury oraz unikalne lokalne i link-local IPv6 są odrzucane, podobnie jak formy IPv6 zawierające adres IPv4.
- Estalo rozwiązuje nazwę, sprawdza wszystkie odpowiedzi i łączy się ze sprawdzonym adresem bez drugiego zapytania DNS, więc odpowiedź DNS zmieniona między sprawdzeniem a połączeniem nie przekieruje żądania do sieci prywatnej.
- Bez danych logowania w adresie URL. Potrzebny token umieść w ścieżce albo w parametrze zapytania, albo polegaj na podpisie.
Dodanie adresu nie wysyła na niego wiadomości testowej. Żeby sprawdzić odbiornik, utwórz zapytanie w biurze testowym i obserwuj dziennik dostaw.
Test odbiornika lokalnie
Estalo nigdy nie wysyła na localhost ani do sieci prywatnej, więc odbiornika na Twoim komputerze nie da się zarejestrować jako adresu. Sprawdź weryfikację podpisu offline, a potem zarejestruj publiczny adres HTTPS (serwer testowy albo tunel z publiczną nazwą DNS) i obserwuj dziennik dostaw.
Weź dowolną wiadomość, którą otrzymał Twój adres, albo zbuduj ją z przykładu w części „Wiadomość” (jedna linia, dokładnie odebrane bajty), znacznik czasu i sekret. Poniższe fragmenty podpisują wiadomość przykładowym sekretem i uruchamiają weryfikację dla poprawnego podpisu, zmienionej treści, złego sekretu i starego znacznika czasu. Zastąp przykład sekretem, który pokazało Ci Estalo; nigdy nie wklejaj prawdziwego sekretu do zgłoszenia ani wspólnego dokumentu.
Node.js (korzysta z verifyEstalo powyżej):
const secret = "whsec_ZASTAP_SWOIM_SEKRETEM";
const rawBody = '{"id":"6f1c2b9e-0000-4000-8000-000000000000","type":"lead.created","schema_version":1,"sequence":18342,"created_at":"2026-10-09T10:15:02.114+00:00","organization_id":"00000000-0000-4000-8000-000000000000","data":{"resource":"lead","id":"a5d0f0c4-3b1e-4f2a-9c8d-0e1f2a3b4c5d","version":"2026-10-09T10:15:02.114+00:00"}}';
const timestamp = String(Math.floor(Date.now() / 1000));
const header = "v1=" + createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
console.log(verifyEstalo(secret, rawBody, timestamp, header)); // true
console.log(verifyEstalo(secret, rawBody + " ", timestamp, header)); // false: zmieniona treść
console.log(verifyEstalo("whsec_INNY", rawBody, timestamp, header)); // false: zły sekret
console.log(verifyEstalo(secret, rawBody, "1700000000", header)); // false: stary znacznik czasu
Python (korzysta z verify_estalo powyżej):
import time
secret = "whsec_ZASTAP_SWOIM_SEKRETEM"
raw_body = b'{"id":"6f1c2b9e-0000-4000-8000-000000000000","type":"lead.created","schema_version":1,"sequence":18342,"created_at":"2026-10-09T10:15:02.114+00:00","organization_id":"00000000-0000-4000-8000-000000000000","data":{"resource":"lead","id":"a5d0f0c4-3b1e-4f2a-9c8d-0e1f2a3b4c5d","version":"2026-10-09T10:15:02.114+00:00"}}'
timestamp = str(int(time.time()))
header = "v1=" + hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
print(verify_estalo(secret, raw_body, timestamp, header)) # True
print(verify_estalo(secret, raw_body + b" ", timestamp, header)) # False: zmieniona treść
print(verify_estalo("whsec_INNY", raw_body, timestamp, header)) # False: zły sekret
Odbiornik powinien też zapamiętywać przetworzone wartości X-Estalo-Event-Id i na powtórkę odpowiadać 2xx bez ponownego wykonywania pracy. Powtórka wygląda tak samo jak pierwsza dostawa, tylko z wyższym X-Estalo-Delivery-Attempt.