header('X-Signature', '')); if ($signature === '') { return ApiError::response( 'signature_missing', 'Signature Missing', 'The RevenueCat webhook request did not include a signature.', Response::HTTP_BAD_REQUEST ); } $payload = $request->getContent(); if (! $this->signatureMatches($payload, $signature, $secret)) { return ApiError::response( 'signature_invalid', 'Invalid Signature', 'The webhook signature could not be validated.', Response::HTTP_BAD_REQUEST ); } $decoded = json_decode($payload, true); if (json_last_error() !== JSON_ERROR_NONE || ! is_array($decoded)) { Log::warning('RevenueCat webhook received invalid JSON', [ 'error' => json_last_error_msg(), ]); return ApiError::response( 'payload_invalid', 'Invalid Payload', 'The webhook payload could not be decoded as JSON.', Response::HTTP_BAD_REQUEST, ['json_error' => json_last_error_msg()] ); } ProcessRevenueCatWebhook::dispatch( $decoded, (string) $request->header('X-Event-Id', '') ); return response()->json(['status' => 'accepted'], 202); } private function signatureMatches(string $payload, string $providedSignature, string $secret): bool { $normalized = preg_replace('/\s+/', '', $providedSignature); if (! is_string($normalized)) { return false; } $candidates = [ base64_encode(hash_hmac('sha1', $payload, $secret, true)), base64_encode(hash_hmac('sha256', $payload, $secret, true)), hash_hmac('sha1', $payload, $secret), hash_hmac('sha256', $payload, $secret), ]; foreach ($candidates as $expected) { if (hash_equals($expected, $normalized)) { return true; } } return false; } }