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:

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:

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:

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:

  1. Nonce — chroni konkretną próbę HTTP przez 10 minut.
  2. Idempotency-Key + hash body — identyfikuje logiczne zdarzenie i dokładne wejście.

Reguły:

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

  1. Nadaj zdarzeniu stabilny Idempotency-Key.
  2. Zserializuj body dokładnie raz.
  3. Wygeneruj timestamp i nowe nonce.
  4. Oblicz podpis nad dokładnym body.
  5. Wyślij żądanie.
  6. 200 albo 202 oznacza sukces.
  7. Dla 409 webhook_in_progress odczekaj Retry-After; zachowaj body i klucz, ale utwórz nowe timestamp, nonce i podpis.
  8. 200 replayed=true oznacza, że wcześniejszy zapis się udał.
  9. Konflikt body, błąd walidacji lub recovery_required zatrzymuje automat.
  10. 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

  1. Wstrzymaj wysyłkę nowych zdarzeń.
  2. Poczekaj na zakończenie trwających prób.
  3. Wygeneruj nowy sekret.
  4. Zapisz go w Deweloper GOV.
  5. Zaktualizuj menedżer sekretów nadawcy.
  6. Wyślij kontrolne zdarzenie z nowym kluczem.
  7. Po sukcesie wznowij kolejkę.
  8. Usuń stary sekret z systemów.

15. Test kontrolny

Test powinien potwierdzić:

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.