# 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](../../operations/watch_create.md) 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](../../operations/watch_webhook_configure.md). 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](../../operations/watch_secret_rotate.md) 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

<!-- example:verify-webhook.py -->
```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ą.

<!-- example:verify-webhook.ts -->
```typescript
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](../../operations/watch_attempts.md) 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](../../operations/billing_stripe_webhook.md) to różne interfejsy i różne sekrety.
