Webhooki PRO — Deweloper GOV 2.1
Dokument opisuje aktualizację mieszkań i domów przez podpisany webhook HMAC. Webhook korzysta z tego samego planera, transakcyjnego importu, historii cen i audytu co import oferty.
POST https://twoja-domena.pl/wp-json/deweloper-gov/v2/webhooks/properties
Wymagane są aktywny plan PRO, entitlement
api_webhooks, produkcyjny endpoint HTTPS oraz
poprawny podpis HMAC-SHA256. HMAC potwierdza integralność i
nadawcę, ale nie szyfruje body — poufność zapewnia TLS. Serwer WWW
lub reverse proxy musi wymuszać HTTPS przed przekazaniem żądania
do WordPressa.
1. Zastosowanie
Webhook jest odpowiedni, gdy CRM lub ERP wysyła pojedynczą zmianę mieszkania albo domu natychmiast po jej zapisaniu.
Webhook nie służy do:
- odczytu katalogu — użyj REST API katalogowego;
- importu pliku — użyj
/imports/offer; - przesyłania części, praw, świadczeń ani zagnieżdżonych struktur;
- publikowania CSV/XML/MD5 — użyj endpointów publikacji.
2. Sekret
Sekret znajduje się w panelu Deweloper GOV w technicznych
parametrach źródła i integracji jako Klucz
webhooka. Jest przechowywany w osobnej opcji WordPress z
autoload=false.
Zasady:
- używaj długiego, losowego sekretu;
- przechowuj go w menedżerze sekretów nadawcy;
- nie przesyłaj go w URL;
- nie używaj go jako Application Password;
- nie zapisuj go w logach, zgłoszeniach ani repozytorium;
- po podejrzeniu ujawnienia zmień sekret po obu stronach.
Zmiana sekretu obowiązuje natychmiast. Wtyczka nie utrzymuje równolegle starego sekretu.
3. Wymagane nagłówki
| Nagłówek | Format | Znaczenie |
|---|---|---|
Content-Type |
application/json |
format payloadu |
X-DG-Timestamp |
dokładnie 10 cyfr | Unix timestamp w sekundach |
X-DG-Nonce |
16–128 znaków [A-Za-z0-9._:-] |
unikalna wartość próby |
Idempotency-Key |
8–128 znaków [A-Za-z0-9._:-] |
stały ID logicznego zdarzenia |
X-DG-Signature |
64 znaki hex | HMAC-SHA256 |
Timestamp może różnić się od zegara WordPressa o maksymalnie 300 sekund. Serwery powinny synchronizować czas przez NTP.
Nonce jest chronione przed ponownym użyciem przez 600 sekund. Każda próba HTTP, także retry, musi dostać nowe nonce.
Idempotency-Key pozostaje taki sam dla tego samego
zdarzenia i body. Nie generuj nowego tylko dlatego, że odpowiedź
HTTP zaginęła.
4. Algorytm podpisu
Podpisywane są dokładne surowe bajty body wysłane przez HTTP.
signing_string = X-DG-Timestamp + "." + X-DG-Nonce + "." + Idempotency-Key + "." + RAW_BODY
signature = hex_lowercase(HMAC_SHA256(signing_string, webhook_secret))
Przykład:
timestamp = 1788343200
nonce = crm-event-attempt-000001
idempotency key = crm-property-A01-version-42
body = {"source_investment_id":"INV-1","source_property_id":"A-01","price_total":"699022.88","price_total_from":"2026-09-02"}
Do podpisu trafia:
1788343200.crm-event-attempt-000001.crm-property-A01-version-42.{"source_investment_id":"INV-1","source_property_id":"A-01","price_total":"699022.88","price_total_from":"2026-09-02"}
Nie serializuj JSON ponownie między podpisem a wysłaniem. Kolejność pól, spacje i końce linii zmieniają podpis.
5. Przykład PHP
<?php
declare(strict_types=1);
$endpoint = 'https://example.com/wp-json/deweloper-gov/v2/webhooks/properties';
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(16));
$idempotencyKey = 'crm-property-A01-version-42';
$payload = [
'source_investment_id' => 'INV-1',
'source_property_id' => 'A-01',
'prop_no' => 'A-01',
'prop_type' => 'mieszkanie',
'price_total' => '699022.88',
'price_total_from' => '2026-09-02',
];
$body = json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$signingString = $timestamp . '.' . $nonce . '.' . $idempotencyKey . '.' . $body;
$signature = hash_hmac('sha256', $signingString, $webhookSecret);
$response = wp_remote_post($endpoint, [
'timeout' => 30,
'headers' => [
'Content-Type' => 'application/json',
'X-DG-Timestamp' => $timestamp,
'X-DG-Nonce' => $nonce,
'Idempotency-Key' => $idempotencyKey,
'X-DG-Signature' => $signature,
],
'body' => $body,
]);6. Przykład Node.js
import crypto from 'node:crypto';
const endpoint = 'https://example.com/wp-json/deweloper-gov/v2/webhooks/properties';
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(16).toString('hex');
const idempotencyKey = 'crm-property-A01-version-42';
const body = JSON.stringify({
source_investment_id: 'INV-1',
source_property_id: 'A-01',
prop_no: 'A-01',
prop_type: 'mieszkanie',
price_total: '699022.88',
price_total_from: '2026-09-02',
});
const signingString = `${timestamp}.${nonce}.${idempotencyKey}.${body}`;
const signature = crypto
.createHmac('sha256', webhookSecret)
.update(signingString, 'utf8')
.digest('hex');
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-DG-Timestamp': timestamp,
'X-DG-Nonce': nonce,
'Idempotency-Key': idempotencyKey,
'X-DG-Signature': signature,
},
body,
});
const result = await response.json();7. Payload
Payload jest płaskim obiektem JSON. Każda wartość musi być
scalar: string, number, boolean albo null. Struktury
zagnieżdżone i tablice odrzucają całe żądanie bez zapisu.
Wymagane pola:
| Pole | Opis |
|---|---|
source_investment_id |
neutralny ID inwestycji w systemie źródłowym |
source_property_id |
ID oferty unikalny w inwestycji |
Pola opcjonalne:
| Pole | Format / znaczenie |
|---|---|
prop_no |
oznaczenie oferty |
prop_type |
mieszkanie albo dom |
property_status |
status oferty |
prop_area |
powierzchnia jako tekst dziesiętny |
prop_rooms |
nieujemna liczba całkowita |
prop_floor |
liczba całkowita, może być ujemna |
price_m2 |
cena brutto za m² jako tekst |
price_m2_from |
data YYYY-MM-DD |
price_total |
łączna cena brutto jako tekst |
price_total_from |
data YYYY-MM-DD |
price_combined |
jawna cena konfiguracji |
price_combined_from |
data konfiguracji |
price_combined_variant |
ID wariantu konfiguracji |
price_combined_status |
stan zatwierdzenia konfiguracji |
proj_voivod |
województwo |
proj_powiat |
powiat |
proj_gmina |
gmina |
proj_city |
miejscowość |
proj_street |
ulica |
proj_build_no |
numer budynku |
proj_local_no |
numer lokalu adresowego |
proj_zip |
kod pocztowy |
Webhook działa w trybie patch: pusta albo
niewysłana wartość nie czyści istniejącego pola.
Kwoty przesyłaj jako string, np. "699022.88", nie
jako zmiennoprzecinkowe 699022.88.
7.1. Ograniczenia
Webhook nie obsługuje:
offer_kind=standalone_part;- samodzielnych miejsc parkingowych, garaży i komórek;
- repeaterów części, pomieszczeń, praw i świadczeń;
- galerii i uploadu plików;
- własnych zagnieżdżonych pól.
Takie dane należy dodać w edytorze albo przez osobne rozszerzenie. Wtyczka nie zgaduje ich kwalifikacji prawnej.
8. Poprawna odpowiedź
Pierwsze wykonanie:
HTTP/1.1 202 Accepted
{
"accepted": true,
"result": {
"status": "completed",
"processed": 1,
"created": 0,
"updated": 1,
"archived": 0,
"run_id": 72
}
}Ponowienie tego samego zdarzenia z tym samym body i kluczem, ale nowym timestampem, nonce i podpisem:
HTTP/1.1 200 OK
{
"accepted": true,
"result": {
"status": "completed",
"processed": 1,
"created": 0,
"updated": 1,
"archived": 0,
"run_id": 72
},
"replayed": true
}Drugie żądanie nie zapisuje ponownie danych ani historii ceny.
9. Replay i idempotencja
Webhook chronią dwie warstwy:
- Nonce — chroni konkretną próbę HTTP przez 10 minut.
- Idempotency-Key + hash body — identyfikuje logiczne zdarzenie i dokładne wejście.
Reguły:
- ten sam nonce drugi raz →
401 replay; - ten sam klucz i body po zakończeniu → wcześniejszy wynik z
replayed=true; - ten sam klucz i inne body →
409 idempotency_body_conflict; - wykonanie trwa →
409 webhook_in_progress,Retry-After: 5; - wygasła dzierżawa bez pewnego wyniku →
409 recovery_required; - potwierdzony błąd pozwala na retry z nowym nonce;
- zakończony wynik jest przechowywany 7 dni.
Zalecany klucz:
{system}:{typ-zdarzenia}:{id-obiektu}:{wersja-lub-id-zdarzenia}
Przykład: crm:property.updated:A-01:000042.
10. Błędy uwierzytelnienia
| HTTP | error |
Przyczyna | Działanie |
|---|---|---|---|
401 |
invalid_auth |
brak nagłówków albo zły format | popraw nagłówki |
401 |
expired_timestamp |
zegar poza ±300 s | zsynchronizuj czas, nowe timestamp i nonce |
401 |
invalid_signature |
body albo sekret nie zgadza się | sprawdź surowe body i sekret |
401 |
replay |
nonce już użyte | nowe nonce, ten sam klucz zdarzenia |
403 |
pro_feature_required |
brak PRO/entitlementu | sprawdź licencję |
11. Błędy wykonania
| HTTP | error |
Znaczenie | Retry |
|---|---|---|---|
400 |
invalid_json |
body nie jest obiektem JSON | po poprawie |
400 |
invalid_payload |
nie utworzono mapowania | po poprawie |
409 |
webhook_in_progress |
wykonanie trwa | po Retry-After, nowe nonce |
409 |
idempotency_body_conflict |
klucz ma inne body | nowy klucz dla nowego zdarzenia |
409 |
recovery_required |
wynik niepewny | ręczna kontrola |
422 |
unsupported_property_structure |
zagnieżdżona/nieobsługiwana struktura | po zmianie payloadu |
422 |
validation_failed |
błąd danych | po naprawie issues |
422 |
webhook_import_failed |
import odrzucony | po usunięciu przyczyny |
503 |
execution_storage_unavailable |
brak trwałej idempotencji | później, bez zmiany klucza |
503 |
recovery_required |
nie potwierdzono wyniku | ręczna kontrola |
12. Algorytm nadawcy
- Nadaj zdarzeniu stabilny
Idempotency-Key. - Zserializuj body dokładnie raz.
- Wygeneruj timestamp i nowe nonce.
- Oblicz podpis nad dokładnym body.
- Wyślij żądanie.
200albo202oznacza sukces.- Dla
409 webhook_in_progressodczekajRetry-After; zachowaj body i klucz, ale utwórz nowe timestamp, nonce i podpis. 200 replayed=trueoznacza, że wcześniejszy zapis się udał.- Konflikt body, błąd walidacji lub
recovery_requiredzatrzymuje automat. - Nigdy nie zmieniaj payloadu pod tym samym kluczem.
13. Logowanie i prywatność
Można logować własny ID zdarzenia, HTTP status, kod błędu, czas
trwania i skrót SHA-256 body. Nie loguj sekretu,
X-DG-Signature, pełnych nagłówków, Application
Password ani danych osobowych.
Wtyczka nie wymaga danych nabywców. Webhook powinien przesyłać wyłącznie dane oferty.
14. Rotacja sekretu
- Wstrzymaj wysyłkę nowych zdarzeń.
- Poczekaj na zakończenie trwających prób.
- Wygeneruj nowy sekret.
- Zapisz go w Deweloper GOV.
- Zaktualizuj menedżer sekretów nadawcy.
- Wyślij kontrolne zdarzenie z nowym kluczem.
- Po sukcesie wznowij kolejkę.
- Usuń stary sekret z systemów.
15. Test kontrolny
Test powinien potwierdzić:
- poprawny podpis daje
202; - drugi request z tym samym nonce daje
401 replay; - ponowienie z nowym nonce, tym samym kluczem i body daje
200 replayed=true; - ten sam klucz z innym body daje
409; - stary timestamp daje
401 expired_timestamp; - błędny podpis daje
401 invalid_signature; - zła cena daje
422 validation_failedbez zmiany oferty; - równoległe żądania nie tworzą podwójnej historii.
16. REST v1
Starszy /wp-json/dg/v1/generate używa prostego
X-DG-Webhook-Key, nie HMAC. Jest utrzymany
przejściowo i nie powinien być wzorem dla nowych webhooków.
Dokumentacja całego API: REST-API-PRO.md.