Zdarzenia, obserwacje i webhooki
/v1/events służy do płatnego odczytu historii zdarzeń. Obserwacja wybiera przyszłe zmiany, które serwer może wysłać na Twój webhook. Brak powiadomienia nie dowodzi braku zmiany: obserwacja może być wstrzymana, dane niepełne lub dostarczenie niedostępne.
Obecnie czytaj historię przez GET /v1/events z q, id lub point. POST /v1/events/query ma schemat, ale jego selektory w body nie są jeszcze obsługiwane i poprawna próbka parcel_ids kończy się 400. Parametry v2 area i parcel_ids w GET również nie zawężają tego odczytu; nie używaj ich jako filtra.
Utwórz obserwację i odbiornik
POST /v1/watches przyjmuje elementy z parcel_id, gmina_teryt albo wielokątem i filtrami types. Jeden element zajmuje jedno miejsce limitu; element gminny nie jest rozwijany na płatną liczbę działek. Idempotency-Key pozwala bezpiecznie ponowić utworzenie z tymi samymi elementami przez 24 godziny. Zmienione elementy dają 409; usunięta obserwacja nie zostaje odtworzona.
Obsługiwane filtry: pog.changed, wz.changed, mpzp.changed, flood.changed, landslide.changed, protected.changed, buildings.changed.
Typy EventV2, takie jak gunb_permit czy plan_general_adopted, mają schematy, ale nie mają jeszcze produkcyjnego producenta powiadomień w tej wersji. Ich rejestracja zwraca event_type_has_no_producer. Zdarzenie wykrycia zmiany nie jest samo w sobie datą prawnego wejścia zmiany w życie.
Skonfiguruj URL przez PUT /v1/watches/{watch_id}/webhook. Zapisz result.secret w menedżerze sekretów: pojawia się tylko przy konfiguracji lub rotacji. Zwykły odczyt go nie zwróci. URL musi wskazywać publiczny HTTPS na porcie 443, bez danych logowania i fragmentu; wszystkie odpowiedzi DNS muszą być publiczne. Serwer nie podąża za przekierowaniami. Nie umieszczaj danych właścicieli ani KW w URL.
Konfiguracja rozpoczyna obserwację od bieżącego kursora i anuluje wcześniejszą kolejkę. Rotacja przez POST .../webhook/rotate zachowuje kolejkę, ale od razu zmienia sekret. Przy rotacji uzgodnij zmianę po stronie odbiornika, aby nie odrzucał nowych podpisów. Usunięcie webhooka anuluje oczekujące dostarczenia.
Sprawdź podpis przed odczytem JSON
X-Landfeed-Signature ma format t=<unix>,v1=<hex>. HMAC-SHA256 obejmuje bajty ASCII znacznika czasu, kropkę i dokładne bajty surowego body. Kluczem są bajty sekretu otrzymanego przy konfiguracji. Nie serializuj ponownie JSON przed weryfikacją. Odrzuć znacznik starszy niż 300 sekund lub o ponad 300 sekund w przyszłości. Zadbaj o zgodny zegar odbiornika.
Python
import hashlib
import hmac
import json
import re
def verify_webhook(secret, signature, raw_body, *, now):
"""Verify before parsing JSON. now is the receiver's Unix time in seconds."""
if not isinstance(signature, str):
return False
match = re.fullmatch(r"t=([0-9]{1,12}),v1=([0-9a-f]{64})", signature)
if not match or abs(now - int(match[1])) > 300:
return False
expected = hmac.new(secret.encode("ascii"), match[1].encode("ascii") + b"." + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, match[2])
def receive(secret, signature, raw_body, delivery_id, *, now, persist_once):
if not verify_webhook(secret, signature, raw_body, now=now):
raise ValueError("Invalid or stale webhook signature")
event = json.loads(raw_body)
if event["id"] != delivery_id:
raise ValueError("Delivery id mismatch")
# Atomically insert a unique delivery id and enqueue processing in one transaction.
# persist_once returns False for an already accepted id. Return 2xx in either case.
return persist_once(delivery_id, event)
persist_once musi atomowo zapisać unikalny X-Landfeed-Delivery i zadanie do przetworzenia w jednej transakcji. Duplikat po poprawnym podpisie również potwierdź kodem 2xx. Nie oznaczaj zadania jako przyjętego, zanim trwały zapis się powiedzie.
TypeScript
Poniższa funkcja weryfikuje podpis. W swoim odbiorniku wykonaj ją przed JSON, sprawdź zgodność identyfikatora body z X-Landfeed-Delivery i zastosuj ten sam trwały zapis z deduplikacją.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhook(secret: string, signature: string, rawBody: Buffer, now: number): boolean {
const match = /^t=([0-9]{1,12}),v1=([0-9a-f]{64})$/.exec(signature);
if (!match || Math.abs(now - Number(match[1])) > 300) return false;
const expected = createHmac("sha256", secret)
.update(match[1], "ascii").update(".").update(rawBody).digest();
const received = Buffer.from(match[2], "hex");
return received.length === expected.length && timingSafeEqual(received, expected);
}
Ponowienia i dostępność
Ponowienia zachowują identyfikator i body dostarczenia, ale dostają nowy podpis czasu. Opóźnienie zaczyna się od 30 sekund, rośnie dwukrotnie do godziny, a próby kończą się po 24 godzinach. Dziesięć kolejnych niepowodzeń wyłącza obserwację; niebezpieczny adres wyłącza ją od razu. GET .../attempts pozwala sprawdzić próby bez treści odpowiedzi odbiornika.
Limit elementów wynika z planu i może pozostawać równy zero do decyzji operatora. Wygaśnięcie planu lub przekroczenie wspólnego limitu może wstrzymać dostarczanie. Wznowienie nie dosyła nowych zdarzeń z okresu przerwy. Zarządzanie obserwacjami jest bezpłatne, podobnie jak dostarczenie w obecnej konfiguracji; odczyt /v1/events rozlicza się oddzielnie. Webhook odbiorcy i webhook Stripe to różne interfejsy i różne sekrety.