Webhook Testing in PHP
Receive webhook POST requests in PHP, verify HMAC signatures, and respond quickly
TL;DR: In PHP, read the raw request body with file_get_contents('php://input') before any JSON decoding, this is required for HMAC signature verification. Return a 200 response immediately and process asynchronously if needed.
Basic PHP Webhook Handler
PHP webhook handlers are just plain PHP scripts. The key rule: call file_get_contents('php://input') as the very first operation: before json_decode(), to preserve the raw bytes for signature verification. Calling $_POST or json_decode() first does not destroy the stream in PHP, but consistently reading raw first is the safest habit.
<?php
// Read raw body before json_decode (required for HMAC)
$rawBody = file_get_contents('php://input');
$payload = json_decode($rawBody, true);
$eventType = $_SERVER['HTTP_X_EVENT_TYPE'] ?? '';
// Respond quickly
http_response_code(200);
header('Content-Type: application/json');
echo json_encode(['received' => true])];HTTP headers arrive in $_SERVER with the prefix HTTP_, uppercased, and with hyphens replaced by underscores. So the header X-Event-Type becomes $_SERVER['HTTP_X_EVENT_TYPE'].
HMAC Signature Verification in PHP
PHP's built-in hash_hmac() and hash_equals() functions are everything you need. Use hash_equals() rather than === to prevent timing attacks:
<?php
function verifyWebhookSignature(string $rawBody, string $signature, string $secret): bool {
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signature);
}
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SIGNATURE_256'] ?? '';
$secret = getenv('WEBHOOK_SECRET');
if (!verifyWebhookSignature($rawBody, $signature, $secret)) {
http_response_code(401);
exit('Invalid signature');
}
$payload = json_decode($rawBody, true);
// Process $payload...
http_response_code(200);
header('Content-Type: application/json');
echo json_encode(['received' => true])];Async Processing in PHP
PHP is synchronous by default, but you can flush the response to the client while continuing to execute code. The output-buffering trick below sends the 200 and closes the connection before running your slow processing logic:
<?php
$rawBody = file_get_contents('php://input');
$payload = json_decode($rawBody, true);
// Send the response immediately
http_response_code(200);
header('Content-Type: application/json');
header('Connection: close');
$body = json_encode(['received' => true])];
header('Content-Length: ' . strlen($body));
echo $body;
// Flush and close the connection
if (ob_get_level()) ob_end_flush();
flush();
// Now do slow work: the client has already received 200
ignore_user_abort(true);
processWebhookEvent($payload);For production workloads, prefer pushing to a proper queue (Redis + a worker, SQS, or a job table in your database) rather than relying on the flush trick, it's fragile and depends on the web server respecting the flush. The output-buffer approach works well for low-traffic hooks or simple scripts.
Using Requex During Development
PHP development often runs on a local server (XAMPP, Laravel Valet, DDEV) that isn't publicly reachable. Instead of setting up a tunnel, point the webhook provider at a Requex.me endpoint first.
Requex captures the exact payload the provider sends, including all headers and the raw body. You can then replay that exact request against your local PHP script with curl:
curl -X POST http://localhost/webhook.php \
-H "Content-Type: application/json" \
-H "X-Signature-256: sha256=abc123..." \
-d '{"id":"evt_123","type":"payment.succeeded"}'This means you can iterate on your handler without triggering a real transaction in the provider each time.
Inspect the Exact Payload Before You Code
Capture what the provider actually sends: headers, raw body, signature format, before writing a single line of PHP.
Open Requex →Related guides
Start Testing Webhooks Now
Generate your unique URL and test webhooks instantly. Free, no signup.
Open Webhook Tester →