query('hub_mode', $request->query('hub.mode')); $token = $request->query('hub_verify_token', $request->query('hub.verify_token')); $challenge = $request->query('hub_challenge', $request->query('hub.challenge')); $expected = (string) config('whatsapp.verify_token'); if ($mode === 'subscribe' && $expected !== '' && hash_equals($expected, (string) $token)) { Log::info('WhatsApp webhook verified'); return response((string) $challenge, 200) ->header('Content-Type', 'text/plain'); } Log::warning('WhatsApp webhook verify failed', [ 'mode' => $mode, 'token_match' => $expected !== '' && hash_equals($expected, (string) $token), ]); return response('Forbidden', 403); } /** * POST /api/whatsapp/webhook * Acknowledge quickly; optionally check X-Hub-Signature-256 when app secret is set. */ public function handle(Request $request) { $raw = $request->getContent(); if (!$this->signatureOk($request, $raw)) { Log::warning('WhatsApp webhook invalid signature'); return response('Invalid signature', 403); } $payload = json_decode($raw, true); if (!is_array($payload)) { return response('OK', 200); } try { $this->logEvents($payload); } catch (\Throwable $e) { Log::error('WhatsApp webhook log failed: ' . $e->getMessage()); } return response('OK', 200); } protected function signatureOk(Request $request, $raw) { $secret = trim((string) config('whatsapp.app_secret')); if ($secret === '') { return true; } $header = (string) $request->header('X-Hub-Signature-256', ''); if ($header === '' || strpos($header, 'sha256=') !== 0) { return false; } $expected = 'sha256=' . hash_hmac('sha256', $raw, $secret); return hash_equals($expected, $header); } protected function logEvents(array $payload) { $entries = $payload['entry'] ?? []; if (!is_array($entries)) { return; } foreach ($entries as $entry) { $changes = $entry['changes'] ?? []; if (!is_array($changes)) { continue; } foreach ($changes as $change) { $value = $change['value'] ?? []; if (!is_array($value)) { continue; } foreach ($value['statuses'] ?? [] as $status) { Log::info('WhatsApp status', [ 'id' => $status['id'] ?? null, 'status' => $status['status'] ?? null, 'recipient' => $status['recipient_id'] ?? null, 'errors' => $status['errors'] ?? null, ]); } foreach ($value['messages'] ?? [] as $message) { Log::info('WhatsApp inbound', [ 'from' => $message['from'] ?? null, 'type' => $message['type'] ?? null, 'id' => $message['id'] ?? null, ]); } } } } }