HOWTO · PHP
How to Create a Webhook in PHP
Build a PHP endpoint that receives a webhook POST, validates its JSON payload, and can be tested locally.
On this page
A webhook is an HTTP request that one service sends to a URL on another system when an event occurs. In this example, PHP receives a POST request containing a JSON payload. The endpoint is the receiver; JSON is the data format. Writing a local file does not create a webhook, although a receiver can log or store data after accepting a request.
What a PHP Webhook Endpoint Does
The sending service makes an HTTP request to a public URL you configure, such as https://example.com/webhook.php. Your web server routes that request to a PHP script. The script checks the request method, reads the raw body, interprets the payload, and returns an HTTP status that tells the sender whether the delivery was accepted.
Webhook senders commonly use POST, but the payload format and authentication mechanism depend on the service. This walkthrough expects JSON and illustrates a small event with an event name and order ID. For another sender, use the field names and signature scheme documented by that service. A form submitted by a person is useful for ordinary web input, but it does not demonstrate the server-to-server delivery flow of a webhook.
Create the PHP Webhook Endpoint
Save this as webhook.php in the document root served by PHP. When requested over HTTP, it reads the request body from php://input. When run directly from the command line, it uses a built-in sample body so the parsing path can be inspected without a sender. The command-line path is a local example; it is not a substitute for testing the actual HTTP route and server configuration.
<?php
$isCli = PHP_SAPI === 'cli';
$method = $isCli ? 'POST' : ($_SERVER['REQUEST_METHOD'] ?? '');
$rawBody = $isCli
? '{"event":"order.created","order_id":1042}'
: file_get_contents('php://input');
if (!$isCli) {
header('Content-Type: application/json; charset=utf-8');
}
$respond = static function (int $status, array $data) use ($isCli): void {
if (!$isCli) {
http_response_code($status);
}
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), PHP_EOL;
};
if ($method !== 'POST') {
if (!$isCli) {
header('Allow: POST');
}
$respond(405, ['error' => 'POST required']);
exit;
}
$payload = json_decode($rawBody);
if (json_last_error() !== JSON_ERROR_NONE || !($payload instanceof stdClass)) {
$respond(400, ['error' => 'Expected a JSON object']);
exit;
}
$respond(200, [
'received' => true,
'event' => $payload->event ?? null,
'order_id' => $payload->order_id ?? null,
]);
{
"received": true,
"event": "order.created",
"order_id": 1042
}
Read and Validate the JSON Payload
php://input exposes the raw request body, so the script can decode JSON without first treating it as form data. With the default second argument, json_decode() returns a top-level JSON object as a PHP stdClass instance and a top-level JSON array as a PHP array. json_last_error() distinguishes valid JSON from malformed input, and the instanceof stdClass check accepts only an object. This keeps [] and other arrays out of the successful response, along with scalar JSON values such as strings and numbers.
The example also requires POST and returns a 405 response for another method. A malformed body or a top-level value of the wrong shape receives a 400 response. The successful response echoes only a small, known set of fields, and missing fields appear as null. Adapt that schema to the sending service and validate required fields and their types before acting on them. Do not assume that syntactically valid JSON is trustworthy or complete.
Test the Endpoint Locally
From the directory containing webhook.php, start PHP’s development server with php -S 127.0.0.1:8000. In another terminal, send a POST request to http://127.0.0.1:8000/webhook.php with curl -i -X POST -H 'Content-Type: application/json' --data '{"event":"order.created","order_id":1042}' http://127.0.0.1:8000/webhook.php. The request body is selectable text in the command, and -i includes the HTTP response headers so you can inspect the status as well as the JSON response body.
The script’s command-line mode supplies the same example payload when invoking php webhook.php; it exercises decoding and response construction but does not test HTTP headers, routing, or the sender’s network request. For a full local HTTP check, use the development server and curl request. The built-in server is intended for development and should not be exposed as a production webhook host.
Log, Display, or Store the Payload
During development, inspect only the fields needed to diagnose a delivery. Avoid printing a complete payload into a public response or browser page: it may contain personal data, credentials, or other secrets. If you write diagnostic records, keep them outside the public document root, restrict access, and apply a retention policy. Sanitize values before placing them in a line-oriented log so supplied data cannot forge additional log entries.
For durable processing, validate the event and required fields, then pass the accepted values to the application’s normal storage layer. A database insert is an application-specific next step; this receiver does not connect to SQL or persist the sample payload. Consider recording a sender-provided event ID with a uniqueness constraint so retries do not process the same delivery twice. Return success only after the work that the sender expects to be acknowledged has completed or has been safely queued.
Production Considerations for Webhook Receivers
Configure the sender with an HTTPS URL and verify its signature or authentication token using the sender’s documented method. If signatures cover the raw request body, verify them against the exact bytes before decoding or changing the body. Keep secrets outside source control, compare signatures safely, and reject stale signed requests when the provider supports timestamps or replay protection. A secret URL alone is not a strong authentication method.
Set a request-size limit at the web server or application layer, since decoding an arbitrarily large body can consume memory. Check the content type and the sender’s documented payload shape, and validate every value before using it in a query, file path, command, or downstream request. Avoid logging authorization headers, signatures, or full sensitive payloads. Return clear status codes: permanent malformed requests should not be acknowledged as successful, while transient server failures should allow the sender’s retry policy to work.
Finally, consult the provider’s delivery behavior. Some senders retry when they do not receive a timely successful response, so keep the receiver fast and make processing idempotent. For slower tasks, validate and enqueue the event, then acknowledge it according to the provider’s requirements. Monitor delivery failures and protect the endpoint with the same operational care as other public application routes.