PHP Webhook Receiver From Scratch
Webhooks are the backbone of real-time communication between services. Whether you're integrating with a payment gateway, a version control system, or a CRM, webhooks provide an efficient way for one system to notify another of events as they happen. But receiving and processing these notifications reliably in your PHP application can be trickier than it first appears.
This article will guide you through building a PHP webhook receiver from the ground up. We'll cover the fundamental concepts, practical implementation details, crucial security measures, and common pitfalls, equipping you to build robust webhook handlers.
The Basics: Setting up Your PHP Receiver
At its core, a webhook is just an HTTP POST request. When an event occurs in the source system (e.g., a new commit on GitHub, a successful payment on Stripe), it sends an HTTP POST request to a pre-configured URL – your webhook endpoint. Your PHP application needs to be ready to receive and process this request.
The simplest PHP script to capture a webhook looks something like this:
<?php
// webhook_receiver.php
// Log the request method
error_log("Webhook received! Method: " . $_SERVER['REQUEST_METHOD']);
// Get the raw POST body
$rawPayload = file_get_contents('php://input');
error_log("Raw Payload: " . $rawPayload);
// Get all request headers
$headers = getallheaders();
error_log("Headers: " . print_r($headers, true));
// Attempt to decode JSON payload
$payload = json_decode($rawPayload, true);
if (json_last_error() === JSON_ERROR_NONE) {
error_log("Decoded JSON Payload: " . print_r($payload, true));
} else {
error_log("Payload is not valid JSON or empty.");
}
// Respond to the sender to acknowledge receipt
// A 200 OK status code is generally expected for success.
http_response_code(200);
echo "Webhook received successfully!";
?>
You'd deploy this webhook_receiver.php file to a publicly accessible web server. When the source system sends a webhook, it will hit this script.
Key takeaways from this basic setup:
file_get_contents('php://input'): This is crucial for getting the raw request body, especially for JSON or XML payloads.$_POSTonly populates forapplication/x-www-form-urlencodedormultipart/form-datacontent types. Webhooks commonly useapplication/json.getallheaders(): This function (or$_SERVERsuperglobal for specific headers likeHTTP_X_GITHUB_EVENT) allows you to inspect the request headers, which often contain important metadata like event types or security signatures.http_response_code(200): Always send an appropriate HTTP status code. A 200 OK tells the sender you've successfully received the request. Other codes like 400 (Bad Request) or 500 (Internal Server Error) signal problems.
Making it Robust: Data Handling and Storage
Just logging to error_log isn't a production-ready solution. You need to store and process the webhook data meaningfully.
Most webhooks send JSON payloads. After capturing php://input, you'll typically parse it:
$rawPayload = file_get_contents('php://input');
$data = json_decode($rawPayload, true); // true for associative array
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400); // Bad Request if payload isn't valid JSON
die("Invalid JSON payload.");
}
// Now you can access data like $data['event_type'], $data['user']['id'], etc.
For persistent storage, a database is usually the best choice. It allows for structured storage, querying, and easier management compared to flat files.
Example: Storing in a MySQL database
Let's assume you have a webhooks table with columns like id, event_type, payload, received_at.
<?php
// ... (previous code for capturing and decoding payload) ...
// Basic database connection (use PDO for production!)
$dbHost = 'localhost';
$dbName = 'my_app_db';
$dbUser = 'webhook_user';
$dbPass = 'super_secret_password';
try {
$pdo = new PDO("mysql:host=$dbHost;dbname=$dbName", $dbUser, $dbPass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $pdo->prepare("INSERT INTO webhooks (event_type, payload, received_at) VALUES (:event_type, :payload, NOW())");
// Extract relevant data from the webhook payload
// This will vary greatly depending on the webhook sender
$eventType = $data['event'] ?? 'unknown_event'; // Example for GitHub/Stripe
$payloadJson = json_encode($data); // Store the full payload as JSON string
$stmt->execute([
':event_type' => $eventType,
':payload' => $payloadJson
]);
http_response_code(200);
echo "Webhook received and stored.";
} catch (PDOException $e) {
error_log("Database error: " . $e->getMessage());
http_response_code(500); // Internal Server Error
echo "Failed to store webhook.";
}
?>
Remember to use prepared statements to prevent SQL injection and robust error handling for database operations.
Security Considerations
Receiving data from external sources always presents security risks. You need to verify that the webhook actually came from the expected sender and hasn't been tampered with.
1. Secret Tokens and Signature Verification
Many services provide a "secret token" that you can use to verify the authenticity of a webhook. The sender uses this secret to generate a cryptographic signature (often HMAC-SHA256) of the request body, which they include in a request header. You then perform the same calculation on your end and compare the results.
Example: Verifying a GitHub Webhook Signature
GitHub sends a X-Hub-Signature-256 header (or X-Hub-Signature for SHA1) which contains a hash of the payload, signed with your secret token.
<?php
// ... (previous code for capturing rawPayload) ...
$secret = 'your_github_webhook_secret_token'; // Get this from your environment variables, NOT hardcoded!
$signatureHeader = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';
if (empty($signatureHeader)) {
http_response_code(403);
die("Signature header missing.");
}
list($algo, $signature) = explode('=', $signatureHeader, 2);
if ($algo !== 'sha256') {
http_response_code(403);
die("Unsupported signature algorithm.");
}
$calculatedSignature = hash_hmac('sha256', $rawPayload, $secret);
if (!hash_equals($signature, $calculatedSignature)) {
http_response_code(403); // Forbidden
die("Signatures do not match. Webhook not authentic.");
}
// If we reach here, the signature is valid. Proceed with processing.
error_log("GitHub webhook signature verified successfully.");
// ... (process payload) ...
?>
Always use hash_equals() for comparing cryptographic hashes to prevent timing attacks.
2. IP Whitelisting (with caution)
Some services publish a list of IP addresses from which their webhooks originate. You could configure your firewall or Nginx/Apache to only allow requests from these IPs. However, this is less common now, as services often use dynamic IPs or CDNs, making whitelisting brittle. Signature verification is generally more reliable.
3. Input Validation
Even after verifying the sender, always validate and sanitize the actual data within the payload before using it in your application or storing it in your database. Don't trust any external input implicitly.
Error Handling and Reliability
Webhooks operate asynchronously, and things can go wrong. Your receiver needs to be resilient.
- HTTP Status Codes: Always return an appropriate HTTP status code.
200 OK: Success. The sender knows you got it.400 Bad Request: Your application couldn't process the request (e.g., invalid JSON, missing required fields).401 Unauthorized/403 Forbidden: Authentication/signature failed.500 Internal Server Error: Something went wrong on your server (e.g., database connection failed, unhandled exception).- Most webhook senders will retry on 5xx errors, and sometimes on specific 4xx errors.
- Idempotency: Webhooks can sometimes be delivered multiple times due to retries or network issues. Design your processing logic to be idempotent, meaning applying the same operation multiple times has the same effect as applying it once. Use a unique ID from the webhook payload (e.g., a
webhook_idorevent_id) to check if you've already processed this specific event. - Asynchronous Processing: Don't do heavy, long-running tasks directly within your webhook receiver. If your processing takes too long, the webhook sender might time out and retry. Instead, quickly save the raw payload to your database or a message queue (like Redis, Rabbit