WordPress REST API w praktyce — budujemy integrację ze sklepu z systemem ERP
Krok po kroku: synchronizacja sklepu WooCommerce z zewnętrznym ERP przez REST API. Custom endpointy, autoryzacja JWT, webhooks, obsługa błędów, retry logic.

Większość agencji WordPress zatrzymuje się na „zainstalujmy WooCommerce i gotowe”. W praktyce każdy sklep powyżej pewnej skali potrzebuje integracji z zewnętrznymi systemami — ERP do zarządzania produktami, CRM do obsługi klientów, system kurierski dla etykiet przewozowych. Tu wchodzi REST API.
Poniżej opisujemy realną implementację z projektu klienta (anonimizowana nazwa): synchronizacja sklepu WooCommerce (3500 produktów, 400 zamówień/dzień) z systemem ERP Comarch Optima przez custom REST endpoints. Co robiliśmy, czego uniknęliśmy, gdzie się potknęliśmy.
Co klient potrzebował
- Produkty: jedno źródło prawdy = ERP. Dodanie / edycja / dezaktywacja w ERP → automatycznie na sklepie w ciągu 5 minut.
- Stany magazynowe: rezerwacja przy zamówieniu (ERP), zwolnienie przy rezygnacji.
- Ceny: różne per grupa klientów (B2B / B2C). ERP trzymał pełną politykę cenową, sklep tylko renderował odpowiednie.
- Zamówienia: PO utworzeniu w WooCommerce → transfer do ERP (z rezerwacją stanu, auto-fakturowanie).
- Klienci: synchronizacja w obie strony (nowy klient B2C na sklepie → konto w ERP; zmiana danych w ERP → aktualizacja konta sklepu).
Wszystko musiało działać niezawodnie — jeden błąd = zdublowana faktura, błędny stan magazynowy, frustrowani klienci.
Architektura — nie „jeden cron co minutę”
Klasyczny błąd: zrobić WP-Cron co 60 sekund, który wyszukuje zmiany w ERP i synchronizuje. Dla 3500 produktów + 100 zmian dziennie = 1440 crona × fetch wszystkiego = zabity ERP i sklep.
Nasza architektura: event-driven + webhooks + idempotency.
ERP Optima
│ webhook po zmianie
▼
Cloudflare Worker (message queue + limit liczby żądań)
│
▼
Custom REST endpoint w WP (/wp-json/devance/v1/sync)
│ JWT auth
│ process event
▼
WooCommerce update + log w tabeli wp_devance_sync_logTrzy poziomy: webhook z ERP → Worker jako buffer → WP endpoint robi pracę. Worker dodaje limit liczby żądań i retry. WP endpoint jest idempotentny (ten sam event = ten sam efekt, więc duplicate webhook OK).
Custom REST endpoint — jak się robi porządnie
Bez wtyczek „WP REST API Helper” — czysty kod w mu-plugin:
<?php
// mu-plugins/erp-sync.php
add_action('rest_api_init', function () {
register_rest_route('devance/v1', '/sync', [
'methods' => 'POST',
'callback' => 'devance_handle_sync',
'permission_callback' => 'devance_verify_jwt',
'args' => [
'event_id' => [
'required' => true,
'validate_callback' => fn($v) => preg_match('/^[a-f0-9-]{36}$/', $v),
],
'type' => [
'required' => true,
'enum' => ['product.updated', 'stock.changed', 'customer.updated'],
],
'payload' => [
'required' => true,
'type' => 'object',
],
],
]);
});
function devance_verify_jwt(WP_REST_Request $request) {
$auth_header = $request->get_header('Authorization');
if (!$auth_header || !preg_match('/Bearer (.+)/', $auth_header, $matches)) {
return new WP_Error('no_token', 'Brak tokenu autoryzacji', ['status' => 401]);
}
try {
$payload = \Firebase\JWT\JWT::decode(
$matches[1],
new \Firebase\JWT\Key(DEVANCE_JWT_SECRET, 'HS256')
);
if ($payload->iss !== 'erp-optima') {
return new WP_Error('invalid_issuer', 'Nieznany nadawca', ['status' => 401]);
}
return true;
} catch (\Throwable $e) {
error_log('[DEVANCE SYNC] JWT decode failed: ' . $e->getMessage());
return new WP_Error('invalid_token', 'Token nieważny', ['status' => 401]);
}
}Kluczowe:
permission_callback, nie__return_true. Każdy endpoint musi autoryzować.- JWT zamiast WordPress cookies / basic auth. ERP tworzy podpisany token, WP weryfikuje.
- Walidacja argumentów —
validate_callback+enum. Nieprawidłowy format = 400 od razu, zanim dojdzie do logiki. - Secret w
wp-config.php, nie w kodzie.define('DEVANCE_JWT_SECRET', '...');
Idempotency — najtrudniejszy problem
Webhook z ERP może być dostarczony 2 razy (network retry, queue worker crashed mid-send). Bez idempotency = dwie identyczne faktury, dwa razy odjęty stan magazynowy.
Rozwiązanie: każdy event ma event_id (UUID). WP trzyma tabelę wp_devance_sync_processed:
CREATE TABLE wp_devance_sync_processed (
event_id CHAR(36) PRIMARY KEY,
processed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
result ENUM('success', 'failed', 'skipped') NOT NULL,
retry_count INT UNSIGNED NOT NULL DEFAULT 0,
error_message TEXT
) ENGINE=InnoDB;Handler na początku sprawdza, czy event już był:
function devance_handle_sync(WP_REST_Request $request) {
global $wpdb;
$event_id = $request->get_param('event_id');
$existing = $wpdb->get_row(
$wpdb->prepare(
"SELECT result FROM wp_devance_sync_processed WHERE event_id = %s",
$event_id
)
);
if ($existing) {
return rest_ensure_response([
'status' => 'duplicate',
'original_result' => $existing->result,
]);
}
// ... actual processing
}Obsługa błędów — retry z exponential backoff
ERP jest czasem niedostępny (maintenance, timeout). Zamiast ślepego retry, exponential backoff:
function devance_fetch_from_erp($endpoint, $params = [], $attempt = 0) {
$max_attempts = 5;
try {
$response = wp_remote_get(DEVANCE_ERP_URL . $endpoint, [
'headers' => ['Authorization' => 'Bearer ' . devance_get_erp_token()],
'timeout' => 10,
'body' => $params,
]);
if (is_wp_error($response)) {
throw new \Exception($response->get_error_message());
}
$code = wp_remote_retrieve_response_code($response);
if ($code >= 500) {
throw new \Exception("ERP 5xx: $code");
}
return json_decode(wp_remote_retrieve_body($response), true);
} catch (\Throwable $e) {
if ($attempt < $max_attempts) {
$wait = pow(2, $attempt); // 1, 2, 4, 8, 16 s
sleep($wait);
return devance_fetch_from_erp($endpoint, $params, $attempt + 1);
}
error_log("[DEVANCE SYNC] Exhausted retries for $endpoint: " . $e->getMessage());
throw $e;
}
}Uwaga: sleep() w handlerze REST blokuje worker PHP-FPM. Dla production — lepiej dispatch’nąć do background queue (Action Scheduler albo własna queue), handler zwraca 202 Accepted, a proces w tle robi retry.
Webhooks w drugą stronę — WooCommerce → ERP
WooCommerce ma własne webhooks (WooCommerce → Settings → Advanced → Webhooks). Można rejestrować na zdarzenia order.created, order.updated, customer.created.
Format — JSON POST na podany URL, podpisany HMAC SHA256.
U nas każdy webhook idzie do Cloudflare Worker, który:
- Weryfikuje HMAC signature
- Parsuje payload
- Transformuje do formatu ERP
- POST do ERP API
- Log do Cloudflare KV na potrzeby debugowania
Worker pozwala oddzielić logikę integracji od WP — jeśli transformacja się zepsuje, nie psuje WP; WP po prostu zobaczy 200 OK z Worker, Worker asynchronicznie spróbuje kilka razy.
Monitoring — bo bez niego zepsuje się cicho
Każda integracja ma tabelę logów. Dashboard w admin panelu pokazuje:
- Liczba synced produktów / zamówień dziennie
- Liczba failed + jakie błędy
- Opóźnienie mediana / p95 między event w ERP a effect w WP
- Alert email jeśli failure rate > 5%
Bez tego — po 3 miesiącach klient krzyczy „produktów nie synchronizuje od tygodnia”, ale Ty się dowiesz o tym dopiero gdy klient zadzwoni. Lepiej mieć alert po 10 failed events w godzinie.
Częste błędy (które widzimy u klientów)
1. Brak ograniczanie liczby żądańu
Klient ma 3500 produktów, ERP wysyła webhook per update. Masz aktualizację cenników „promocja” = 3500 webhooks w minutę. WP REST endpoint przyjmuje wszystkie → PHP-FPM zablokowany → cała strona zdycha.
Fix: limit liczby żądań w Cloudflare Worker. Max 50 requests/min, reszta queueing.
2. Autoryzacja przez cookie admina
„Zalogujmy się jako admin w ERP i klikajmy, ERP robi requesty z naszym cookie.” Kruche, niebezpieczne, niemożliwe do zautomatyzowania.
Fix: Application Passwords (od WP 5.6) albo JWT. Nigdy cookie.
3. Synchronous wszystko
Klient robi order → WP czeka na odpowiedź ERP (2 sekundy) → pokazuje „Dziękujemy” → user czeka na page load. Dla paid traffic każda sekunda opóźnienia = mniejsza konwersja.
Fix: async. Po create order, dispatch do queue (Action Scheduler), response od razu. ERP synchronizowany w tle.
4. Brak versionowania API
Endpoint /wp-json/devance/v1/sync. Po roku chcesz zmienić format payload. Stary klient ERP jeszcze używa starego formatu. Łamiesz.
Fix: wersjonuj URL od początku. v1, v2. Utrzymuj starą wersję 6 miesięcy przed wygaszeniem.
Kiedy nie warto pisać własnej integracji
Gotowe wtyczki (WP All Import, Zapier, Make / Integromat) są OK dla small scale (do 500 produktów, do 10 zamówień dziennie) i prostych transformacji. Jeśli klient pasuje, zaczynajcie od nich.
Custom REST integration ma sens gdy:
- Volume > 100 events dziennie
- Custom business logic (rabaty, loyalty points, dynamic pricing)
- Integracja z niestandardowym ERP bez gotowego connectora
W Devance większość integracji robimy custom, bo klienci mają niestandardowe systemy (polski Comarch Optima, Subiekt, enova365 — dla każdego inna komunikacja).
Jeśli planujesz integrację WooCommerce ← → ERP/CRM i nie wiesz od czego zacząć — możemy podzielić się konkretnym planem architektonicznym w ramach konsultacji. Dla stałych klientów pakietu opieki custom integracje robimy w ramach godzin pakietu.

Doświadczony WordPress Developer z ponad 14-letnim stażem w tworzeniu zaawansowanych stron i sklepów internetowych. Specjalizuje się w WordPressie, dedykowanych wtyczkach i motywach.
Więcej o autorze
