service_name, [ 'methods' => 'POST', 'callback' => [$this, 'handleWebhookRequest'], 'permission_callback' => '__return_true', // Webhooks come from external services ]); }); } /** * Handle webhook REST API request */ public function handleWebhookRequest(WP_REST_Request $request): WP_REST_Response { $payload = $request->get_params(); $headers = $request->get_headers(); // Include headers in payload for signature validation $payload['_headers'] = $headers; $success = $this->handleWebhook($payload); return new WP_REST_Response([ 'success' => $success ], $success ? 200 : 400); } public function handleWebhook(array $payload):bool { error_log('[Integrations]::handleWebhook for '.$this->service_name.': '.print_r($payload, true)); if (!$this->validateWebhook($payload)) { $this->logError('handleWebhook', 'Webhook validation failed.'); return false; } //Check for duplicate processing if ($this->isWebhookProcessed($payload)) { return true; } try { $result = $this->processWebhook($payload); if ($result) { $this->markWebhookProcessed($payload); } else { $this->logError('handleWebhook', 'Webhook processing returned false.'); } return $result; } catch (Exception $e) { $this->logError('handleWebhook', 'Webhook processing failed', [ 'error' => $e->getMessage(), 'webhook_id' => $this->extractWebhookId($payload), 'trace' => $e->getTraceAsString() ]); Auth::getInstance()->markUnhealthy($this->service_name, 'oauth', $this->userID); return false; } } protected function renderWebhookUrl(): string { if (!$this->hasWebhooks || !$this->isSetUp()) { return ''; } $webhook_url = rest_url('jvb/v1/webhooks/' . $this->service_name); return sprintf( '

Webhook URL

%s

Add this URL to your %s webhook settings

', esc_attr($this->service_name), esc_html($webhook_url), esc_html($this->title) ); } protected function isWebhookProcessed(array $payload):bool { $id = $this->extractWebhookId($payload); $key = BASE."webhook_processed_{$this->service_name}_{$id}"; return get_transient($key)!==false; } protected function markWebhookProcessed(array $payload):void { $id = $this->extractWebhookId($payload); if (!empty($id)) { $key = BASE."webhook_processed_{$this->service_name}_{$id}"; set_transient($key, true, DAY_IN_SECONDS); } } }