REST API i integracje — Deweloper GOV 2.1

Dokument opisuje publiczny kontrakt integracyjny Deweloper GOV 2.1: katalog danych dla zewnętrznego frontendu, operacje administracyjne, importy, publikację oraz zgodność ze starszym REST v1.

Dokumentację podpisanych webhooków HMAC zawiera WEBHOOKI-PRO.md. Kontrakt maszynowy REST v2 znajduje się w openapi-deweloper-gov-v2.yaml.

1. Zakres i wymagania

Bazowy adres REST v2:

https://twoja-domena.pl/wp-json/deweloper-gov/v2

API wymaga WordPressa 6.4 lub nowszego, HTTPS w integracji produkcyjnej oraz uwierzytelnienia właściwego dla danej grupy endpointów. Katalog, status, wykonanie publikacji i webhook wymagają aktywnego PRO z api_webhooks. Import plików wymaga bulk_import, a preflight wymaga publishing_gov; kod sprawdza entitlement, nie samą nazwę planu.

API nie omija walidacji wtyczki. Importy, publikacja i webhook przechodzą przez te same reguły cen, historii, kontraktu prawnego oraz audytu co panel i cron.

1.1. Mechanizmy dostępu

Grupa Uwierzytelnienie Uprawnienie Przeznaczenie
/catalog/* WordPress Application Password dg_read_catalog odczyt przez backend zewnętrznego frontendu
/status, /imports/*, /publications/* Application Password albo sesja WordPress z nonce manage_options automatyzacje administracyjne i panel
/webhooks/properties HMAC-SHA256 w nagłówkach sekret webhooka zdarzeniowa aktualizacja mieszkania lub domu
/wp-json/dg/v1/generate X-DG-Webhook-Key wewnętrzna kontrola sekretu zgodność ze starszymi integracjami

Application Password i sekret webhooka są niezależne.

2. WordPress Application Password

2.1. Katalog dla zewnętrznego frontendu

  1. Utwórz osobnego użytkownika WordPress.
  2. Nadaj mu rolę Deweloper GOV — odczyt API (dg_catalog_reader).
  3. W profilu utwórz Application Password.
  4. Jednorazowo pokazane hasło zapisz w menedżerze sekretów backendu.
  5. Wywołuj API wyłącznie z serwera, nigdy z JavaScriptu wysyłanego do przeglądarki.
export DG_CATALOG_API_AUTH='api-user:APPLICATION_PASSWORD'
curl --fail-with-body \
  --user "$DG_CATALOG_API_AUTH" \
  'https://example.com/wp-json/deweloper-gov/v2/catalog/properties?status=dostepne&per_page=100'

Wtyczka nie zapisuje Application Password. WordPress przechowuje jego hash i pozwala unieważnić pojedynczy token.

2.2. Endpointy administracyjne

Zewnętrzna automatyzacja powinna używać osobnego konta administratora i osobnego Application Password:

export DG_ADMIN_API_AUTH='automation-admin:APPLICATION_PASSWORD'
curl --fail-with-body \
  --user "$DG_ADMIN_API_AUTH" \
  'https://example.com/wp-json/deweloper-gov/v2/status'

Konto musi mieć manage_options. Konto dg_catalog_reader celowo nie może uruchamiać importów ani publikacji. W panelu WordPress te endpointy mogą być wywoływane przez zalogowaną sesję z poprawnym nonce REST (X-WP-Nonce).

3. Reguły wspólne

Endpoint może zwracać błąd WordPress:

{
  "code": "dg_catalog_forbidden",
  "message": "Użytkownik nie ma uprawnienia do odczytu katalogu.",
  "data": { "status": 403 }
}

albo odpowiedź wtyczki:

{
  "error": "pro_feature_required",
  "feature": "api_webhooks"
}

Klient powinien opierać logikę na kodzie HTTP i polu code albo error, nie na treści komunikatu.

HTTP Znaczenie
200 operacja zakończona lub odczyt zwrócony
202 webhook przyjęty i zapisany
304 katalog nie zmienił się
400 nieprawidłowy format żądania
401 brak albo błędne uwierzytelnienie
403 brak capability, PRO, entitlementu lub HTTPS
404 zasób albo job nie istnieje
409 operacja trwa, wymaga odzyskania albo klucz ma inne wejście
422 błąd walidacji biznesowej
429 limit katalogu; respektuj Retry-After
503 nie można bezpiecznie potwierdzić stanu, blokady albo limitu

4. Katalog dla zewnętrznego frontendu

Katalog zwraca wyłącznie wpisy ze statusem WordPress publish. Pozwala zbudować własną tabelę, grid, popup, galerię, kartę oferty i sekcję podobnych ofert bez bezpośredniego odczytywania bazy.

4.1. Odkrywanie API

GET /catalog
{
  "api_version": "2.1",
  "plugin_version": "2.1.0",
  "contract": {
    "id": "contract-id",
    "valid_until": "2026-11-10"
  },
  "resources": {
    "investments": "https://example.com/wp-json/deweloper-gov/v2/catalog/investments",
    "properties": "https://example.com/wp-json/deweloper-gov/v2/catalog/properties"
  },
  "extension_options": {}
}

4.2. Inwestycje

GET /catalog/investments
GET /catalog/investments/{id}
Parametr Typ Domyślnie Opis
page integer 1 numer strony
per_page integer 20 1–100 wyników
search string wyszukiwanie po nazwie
_fields string projekcja, np. id,title,location

Kolekcja zwraca nagłówki X-WP-Total, X-WP-TotalPages, ETag, Last-Modified i Cache-Control: private, must-revalidate.

Skrót inwestycji zawiera:

Szczegół dodaje content, excerpt, wspólny context, media, dokumenty, widoczność i custom_meta.

4.3. Oferty

GET /catalog/properties
GET /catalog/properties/{id}
Parametr Typ Opis
investment integer ID inwestycji
status string znormalizowany status
offer_kind string property albo standalone_part
type string typ nieruchomości
rooms integer liczba pokoi
floor integer piętro, także ujemne
area_min, area_max decimal string zakres powierzchni
price_min, price_max integer zakres ceny w groszach
search string nazwa lub oznaczenie
modified_after date-time zmiany późniejsze niż podany czas
orderby enum modified, title, price, area
order enum ASC albo DESC
page, per_page integer paginacja, maksimum 100
_fields string projekcja, np. id,title,price.cents

Skrót oferty zawiera dane identyfikacyjne, inwestycję, typ, status, powierzchnię, pokoje, piętro, cenę, cenę za m², miniaturę i czas modyfikacji.

Szczegół zawiera:

Pola wspólnego obiektu offer:

Pole Typ Znaczenie
id integer ID wpisu WordPress
title, code string nazwa i oznaczenie oferty
offer_kind string property albo standalone_part
property_type string typ lokalu, domu albo samodzielnej części
is_standalone_part boolean czy oferta jest samodzielną częścią
offer_type_label string czytelna etykieta typu
url URI publiczny permalink
status, status_label string kod i czytelna nazwa statusu
area, area_label string wartość źródłowa i prezentacyjna
rooms, floor, building string parametry oferty; mogą być puste dla samodzielnej części
price, price_cents string, integer cena prezentacyjna i grosze
price_m2, price_m2_cents string, integer cena za m²; pusta/0, gdy nie dotyczy
price_date, price_m2_date string sformatowane daty obowiązywania
variant, variant_label string techniczny wariant i czytelna etykieta
variant_components string[] czytelne składniki wariantu
image URI/string URL miniatury
gallery integer[] ID załączników; pełne media są w media.gallery
investment_id integer powiązana inwestycja

Każda pozycja components zawiera group, description, prezentacyjną price, date, relation, requirement, variant oraz legal_title.

Obiekt contact zawiera developer_name, phone, email i sales_office_address. Nie zawiera danych nabywców.

{
  "price": {
    "cents": 69902288,
    "formatted": "699 022,88 zł",
    "valid_from": "2026-08-25"
  }
}

4.4. Historia cen

GET /catalog/properties/{id}/price-history
{
  "property_id": 207,
  "items": [
    {
      "record_type": "change",
      "price_type": "price_total",
      "valid_from": "2026-08-25",
      "recorded_at": "2026-08-25",
      "before": { "cents": 69900000, "formatted": "699 000,00 zł" },
      "after": { "cents": 69902288, "formatted": "699 022,88 zł" },
      "label": "Zmiana historyczna",
      "reason": "",
      "correction": false
    }
  ]
}

before.cents albo after.cents może być null, np. przy pierwszej cenie lub wycofaniu ceny. Wartość X nie jest przedstawiana jako kwota.

4.5. Podobne oferty

GET /catalog/properties/{id}/related?page=1&per_page=6

Zwraca inne opublikowane oferty z tej samej inwestycji w formacie skróconej kolekcji.

4.6. Cache warunkowy

curl --user "$DG_CATALOG_API_AUTH" \
  -H 'If-None-Match: "WCZESNIEJSZY_ETAG"' \
  'https://example.com/wp-json/deweloper-gov/v2/catalog/properties'

Obsługiwane są mocne i słabe ETag, lista wartości oraz If-Modified-Since. Niezmieniona odpowiedź ma status 304.

4.7. Limit i CORS

Katalog ma limit 120 żądań na minutę dla pary użytkownik–IP. Po przekroczeniu zwraca 429 z Retry-After. Jeśli nie może atomowo potwierdzić licznika, zwraca 503.

Wtyczka celowo nie otwiera CORS. Zalecany przepływ:

przeglądarka → backend strony klienta → Deweloper GOV REST API

5. Status systemu

GET /status

Wymaga manage_options, aktywnego PRO i api_webhooks.

{
  "version": "2.1.0",
  "contract_id": "contract-id",
  "license_status": "active",
  "continuity_mode": false,
  "last_publication": {},
  "public_dataset_id": "12345"
}

6. Import oferty CSV/XLSX

Import jest dwuetapowy: podgląd i pełna walidacja, potem świadome wykonanie. Podgląd niczego nie zapisuje w ofertach.

Wymagania:

6.1. Pola kanoniczne

source_investment_id, source_property_id, prop_no, prop_type,
property_status, prop_area, prop_rooms, prop_floor,
price_m2, price_m2_from, price_total, price_total_from,
price_combined, price_combined_from, price_combined_variant,
price_combined_status, proj_voivod, proj_powiat, proj_gmina,
proj_city, proj_street, proj_build_no, proj_local_no, proj_zip

prop_type, jeśli podany, musi być mieszkanie albo dom.

6.2. Podgląd

curl --fail-with-body \
  --user "$DG_ADMIN_API_AUTH" \
  -F 'file=@oferta.xlsx' \
  -F 'mode=patch' \
  -F 'mapping={"source_investment_id":"ID inwestycji","source_property_id":"ID lokalu","prop_no":"Numer","price_total":"Cena brutto"}' \
  'https://example.com/wp-json/deweloper-gov/v2/imports/offer'
{
  "id": "6c01b012304f4dcdb5de0c8a7851104b",
  "preview": {
    "valid": true,
    "mode": "patch",
    "operations": [
      {
        "action": "upsert",
        "key": "INV-1:A-01",
        "values": {
          "source_investment_id": "INV-1",
          "source_property_id": "A-01",
          "price_total": "699022.88"
        }
      }
    ],
    "errors": []
  }
}

Niepoprawny podgląd zwraca 422, ID joba i błędy row, field, code, message. Nie wykonuj joba z valid=false.

6.3. Wykonanie

curl --fail-with-body \
  --user "$DG_ADMIN_API_AUTH" \
  -H 'Idempotency-Key: import-crm-2026-09-02-001' \
  --data-urlencode 'job_id=6c01b012304f4dcdb5de0c8a7851104b' \
  --data-urlencode 'confirm_archiving=false' \
  'https://example.com/wp-json/deweloper-gov/v2/imports/offer'

Dla snapshotu z archiwizacją ustaw confirm_archiving=true dopiero po pokazaniu zakresu operatorowi.

{
  "id": "6c01b012304f4dcdb5de0c8a7851104b",
  "result": {
    "status": "completed",
    "processed": 120,
    "created": 2,
    "updated": 118,
    "archived": 0,
    "run_id": 71
  }
}

Import jest atomowy. completed oznacza trwale zatwierdzony wynik. import_pending, import_conflict i import_recovery_required zwracają 409, a trwająca operacja także Retry-After: 5.

7. Import gotowego raportu urzędowego

Import raportu nie tworzy i nie aktualizuje ofert ani frontendu. Przekształca CSV/XLSX do kanonicznego CSV, wymaga 58 kolumn i pełnej walidacji kontraktu.

7.1. Podgląd

curl --fail-with-body \
  --user "$DG_ADMIN_API_AUTH" \
  -F 'file=@raport.xlsx' \
  'https://example.com/wp-json/deweloper-gov/v2/imports/report'

Odpowiedź zawiera id oraz preview: valid, techniczne csv_path, row_count, warning i errors. Klient nie powinien zapisywać ani interpretować csv_path.

7.2. Potwierdzenie

curl --fail-with-body \
  --user "$DG_ADMIN_API_AUTH" \
  --data-urlencode 'job_id=e1c5bb7edbde49f29e14a69e54b737dd' \
  --data-urlencode 'confirm=true' \
  --data-urlencode 'date=2026-09-02' \
  'https://example.com/wp-json/deweloper-gov/v2/imports/report'

Brak potwierdzenia, pliku albo joba zwraca 422 report_confirmation_required lub 404 import_not_found.

8. Status joba

GET /imports/{id}?page=1&per_page=100

per_page ma maksimum 200. Odpowiedź zawiera id, type, mode, status, operatora, liczniki, daty, result, stronicowane items, page, per_page i total_items.

Statusy:

Podglądy są przechowywane 24 godziny, zakończone wyniki 7 dni. Wygasłe joby są usuwane przez retencję. Retencja nie usuwa historii cen.

9. Preflight publikacji

POST /publications/preflight

Parametry: opcjonalna date oraz verify_portal. Odpowiedź rozdziela valid, production_allowed, kontrakt, liczbę rekordów, mode, artifact_status, delivery_status, delivery_checked_at, delivery_issues, portal_status, issues i opcjonalny wynik portal.

curl --fail-with-body \
  --user "$DG_ADMIN_API_AUTH" \
  -H 'Content-Type: application/json' \
  -d '{"date":"2026-09-02","verify_portal":false}' \
  'https://example.com/wp-json/deweloper-gov/v2/publications/preflight'

Przy 422 pole issues zawiera stabilny kod i komunikat naprawczy.

10. Publikacja

POST /publications
curl --fail-with-body \
  --user "$DG_ADMIN_API_AUTH" \
  -H 'Content-Type: application/json' \
  -d '{"date":"2026-09-02"}' \
  'https://example.com/wp-json/deweloper-gov/v2/publications'

Endpoint uruchamia wspólny atomowy pipeline. Nie wywołuj go równolegle z cronem ani importem raportu. Błąd zwraca 422 i nie powinien usuwać poprzedniego poprawnego snapshotu. Przed publikacją zalecany jest jawny preflight.

11. Webhook właściwości

POST /webhooks/properties

Webhook używa HMAC i trwałej idempotencji, nie Application Password. Pełny opis: WEBHOOKI-PRO.md.

12. REST v1 — zgodność przejściowa

GET|POST /wp-json/dg/v1/generate
curl --fail-with-body \
  -H 'X-DG-Webhook-Key: WEBHOOK_SECRET' \
  'https://example.com/wp-json/dg/v1/generate?date=2026-09-02'

Data musi być realną datą YYYY-MM-DD. Endpoint wymaga PRO i api_webhooks.

Parametr ?key= jest domyślnie wyłączony. Przejściowe włączenie dodaje nagłówki Deprecation i Warning oraz wpis audytowy. Query key zostanie usunięty w 3.0.0, ponieważ sekrety w URL trafiają do historii i logów.

13. Bezpieczne pola dodatkowe

custom_meta może zawierać pola Carbon Fields, pola ACF jawnie oznaczone do REST, meta z show_in_rest=true i klucze wskazane filtrem dg_rest_business_meta_keys.

Wtyczka bezwarunkowo odrzuca hasła, tokeny, nonce, sekrety, klucze licencyjne i techniczne prefiksy, m.in. _edit_*, _wp_*, _oembed_*, _elementor_*, _transient_*, _esticrm_*. Nie serializuje obiektów PHP, zasobów ani nadmiernych struktur.

14. Rozszerzanie API i szablonów

<?php

declare(strict_types=1);

use Carbon_Fields\Container;
use Carbon_Fields\Field;

add_action('carbon_fields_register_fields', static function (): void {
    Container::make('post_meta', 'Marketing')
        ->where('post_type', '=', 'dg_property')
        ->add_fields([
            Field::make('text', 'marketing_badge', 'Etykieta marketingowa'),
        ]);
});

add_filter('dg_frontend_property_data', static function (array $data, int $postId): array {
    $data['marketing_badge'] = (string) carbon_get_post_meta($postId, 'marketing_badge');
    return $data;
}, 10, 2);

add_filter('dg_rest_business_meta_keys', static function (array $keys, string $postType): array {
    if ($postType === 'dg_property') {
        $keys[] = 'marketing_badge';
    }
    return $keys;
}, 10, 2);

add_action('dg_after_property_details', static function (int $postId): void {
    $badge = (string) carbon_get_post_meta($postId, 'marketing_badge');
    if ($badge !== '') {
        echo '<p class="custom-badge">' . esc_html($badge) . '</p>';
    }
});

Globalna opcja:

add_filter('dg_extension_settings_fields', static fn (array $fields): array => $fields + [
    'marketing_phone' => [
        'type' => 'text',
        'label' => 'Telefon kampanii',
        'description' => 'Numer używany przez zewnętrzny frontend.',
        'default' => '',
        'sanitizer' => 'sanitize_text_field',
        'expose_in_catalog' => true,
    ],
]);

Bez expose_in_catalog=true opcja nie trafia do /catalog.

Stabilne filtry:

Dostępne są pary dg_before_* i dg_after_* dla listing, investment, property oraz sekcji details, price_history, documents i contact.

15. Klient Node.js po stronie serwera

const endpoint = new URL('/wp-json/deweloper-gov/v2/catalog/properties', baseUrl);
endpoint.searchParams.set('per_page', '100');

const authorization = Buffer.from(`${username}:${applicationPassword}`).toString('base64');
const response = await fetch(endpoint, {
  headers: {
    Authorization: `Basic ${authorization}`,
    Accept: 'application/json',
  },
});

if (!response.ok) {
  throw new Error(`Deweloper GOV API: HTTP ${response.status}`);
}

const properties = await response.json();

16. Lista kontrolna wdrożenia

17. Wersjonowanie

Zmiany addytywne pozostają w v2. Usunięcie pola, zmiana jego znaczenia albo zasad uwierzytelnienia wymaga nowego namespace, np. v3.

Maszynowy kontrakt: openapi-deweloper-gov-v2.yaml.