PHP
PHP Webhook Receiver with Signature Verify
Build a secure PHP webhook receiver that validates incoming payloads by verifying the signature provided by the sender, ensuring data integrity and authenticity.
<?php
// --- Configuration ---
$secret = getenv('WEBHOOK_SECRET') ?: 'your_super_secret_key'; // Use environment variable for production!
$logFile = __DIR__ . '/webhook.log';
$allowedMethods = ['POST']; // Webhooks typically use POST
// --- Function to log events ---
function logMessage($message) {
global $logFile;
file_put_contents($logFile, date('[Y-m-d H:i:s] ') . $message . PHP_EOL, FILE_APPEND);
}
logMessage('Webhook receiver started for ' . $_SERVER['REQUEST_URI']);
// --- Validate Request Method ---
if (!in_array($_SERVER['REQUEST_METHOD'], $allowedMethods)) {
header('HTTP/1.1 405 Method Not Allowed');
logMessage('Error: Method ' . $_SERVER['REQUEST_METHOD'] . ' not allowed.');
exit('Method Not Allowed');
}
// --- Get Raw Payload ---
$payload = file_get_contents('php://input');
if ($payload === false) {
header('HTTP/1.1 400 Bad Request');
logMessage('Error: Could not read raw payload.');
exit('Bad Request');
}
// --- Signature Verification (Example for 'X-Hub-Signature' like GitHub/Stripe) ---
// The exact header name and algorithm (e.g., sha1, sha256) depend on the webhook provider.
$signatureHeader = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? $_SERVER['HTTP_X_HUB_SIGNATURE'] ?? null;
if ($signatureHeader) {
// Example: For GitHub, header format is 'sha1=hex_digest'
// For Stripe, header format is 'v1=hex_digest' with timestamp
// For this example, let's assume it's directly the hex digest, or 'sha256=hex_digest'
$parts = explode('=', $signatureHeader, 2);
$algo = count($parts) > 1 ? $parts[0] : 'sha256'; // Default to sha256 if no algo specified
$providedSignature = count($parts) > 1 ? $parts[1] : $signatureHeader;
$expectedSignature = hash_hmac($algo, $payload, $secret);
// Use hash_equals for timing attack resistance
if (!hash_equals($expectedSignature, $providedSignature)) {
header('HTTP/1.1 401 Unauthorized');
logMessage('Error: Invalid signature. Expected ' . $expectedSignature . ', Got ' . $providedSignature);
exit('Invalid Signature');
}
logMessage('Signature verified successfully.');
} else {
// Depending on your webhook provider, signature might be optional or in a different header.
// For critical webhooks, it should always be required.
logMessage('Warning: No signature header found. Processing without verification (DANGEROUS for production!).');
// If signature is mandatory, uncomment the following:
// header('HTTP/1.1 401 Unauthorized');
// exit('Signature Required');
}
// --- Process the Payload ---
$data = json_decode($payload, true);
if (json_last_error() !== JSON_ERROR_NONE) {
header('HTTP/1.1 400 Bad Request');
logMessage('Error: Invalid JSON payload. ' . json_last_error_msg());
exit('Invalid JSON');
}
logMessage('Payload received and decoded successfully: ' . json_encode($data));
// --- Your application logic goes here ---
// Example:
if (isset($data['event_type']) && $data['event_type'] === 'order.created') {
logMessage('New order created: ' . $data['order_id']);
// Trigger internal processes, update database, send notifications, etc.
} else {
logMessage('Unhandled event type or payload structure.');
}
// --- Respond to webhook sender ---
header('Content-Type: application/json');
echo json_encode(['status' => 'success', 'message' => 'Webhook received and processed.']);
logMessage('Webhook processed and response sent.');
?>
How it works: This PHP snippet demonstrates how to implement a secure webhook receiver. It safely reads the incoming raw JSON payload, verifies its authenticity by comparing a calculated HMAC signature against the one provided in the request headers (e.g., `X-Hub-Signature`), and then processes the decoded data. Crucially, it uses `hash_equals` for timing-attack safe comparison and logs all major steps, making it a robust foundation for integrating with external services via webhooks. Remember to secure `WEBHOOK_SECRET` using environment variables.