Webhook Testing in PHP
Receive webhook POST requests in PHP, verify HMAC signatures, and respond quickly — from basic handler to production-ready patterns.
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 →