Many applications appear to work automatically because there are webhooks behind the scenes. When a payment is successful, the payment service sends a notification to the website. When a user fills out a form, another application receives the data without having to check every few seconds. It's convenient, but webhooks are not just URLs that can receive requests.
The webhook endpoint deals with data from other systems, networks that are not always stable, and the possibility of requests being resent. If designed too simply, the application could process fake data, store duplicate transactions, or lose important information when the server is experiencing issues.
What exactly is a webhook?
A webhook is a communication mechanism where a service sends an HTTP request to your application's URL after an event occurs. Unlike polling—where your application repeatedly asks if there is new data—a webhook allows the sender to take the initiative.
For example, a payment service might send a request like the following after a transaction is completed:
POST /webhooks/payment HTTP/1.1
Content-Type: application/json
{
"event": "payment.completed",
"transaction_id": "TRX-12345",
"amount": 150000,
"status": "paid"
}Your website then reads this data, updates the order status, and may send a confirmation email to the customer.
Common issues with webhooks
1. Fake requests
If the webhook URL is publicly accessible without verification, anyone who knows the address can send requests as if they came from an official service. The impact can be serious: order statuses change, balances increase illegitimately, or internal processes run without permission.
2. Same request processed multiple times
The webhook sender will typically try to resend the request if the server's response is not received properly. This is normal behavior because the network can disconnect after the application processes the data, but before the response reaches the sender.
Without protection, a single payment could be recorded twice, an email could be sent multiple times, or stock could decrease more than it should.
3. Unexpected payload
The payload is the data content sent in the request. Do not assume that all fields are always available or have the correct data types. Configuration errors, API version changes, or incomplete data can cause the code to fail mid-process.
4. Server takes too long to respond
The webhook endpoint should not perform too much work before sending a response. If the process involves generating reports, sending emails, and calling several other APIs, the sender may assume the request failed and resend it.
First layer: verify that the sender is legitimate
A common method used is a signature or digital signature. The sender creates a hash of the request content using a secret key known only to both systems. The receiving application calculates the same hash and then compares it with the signature sent in the header.
In simple terms, the flow is as follows:
- Take the raw request body, not the decoded result that has been reformatted.
- Combine the body with the secret key using an agreed-upon algorithm, such as HMAC-SHA256.
- Compare the calculated result with the signature from the header.
- Reject the request if the signature does not match.
An example check in PHP might look like this:
$payload = file_get_contents('php://input');
$receivedSignature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$secret = $_ENV['WEBHOOK_SECRET'];
$expectedSignature = hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expectedSignature, $receivedSignature)) {
http_response_code(401);
exit('Invalid signature');
}The header name and signature format will depend on the service used. The webhook provider's documentation should be the primary reference.
Validate data before touching the database
A valid signature only proves that the request was made by someone who has the secret. It does not mean that the data content is safe to process directly.
Check several important things:
- Is the received event of a type that you actually support?
- Are the required fields present?
- Is the data type correct, for example, is the amount a number?
- Is the transaction status in line with the business flow?
- Does the transaction ID have a reasonable format?
Validation should also be performed on the server side. Do not rely on JavaScript validation or assume that the sending service always sends perfect data.
Use event IDs to prevent duplicate processing
Each webhook event ideally has a unique ID. Store this ID in the database before or alongside the related business process. When an event with the same ID comes in again, the application can recognize it as a request that has already been processed.
For example, create a simple table:
CREATE TABLE webhook_events (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
event_id VARCHAR(100) NOT NULL UNIQUE,
event_type VARCHAR(100) NOT NULL,
received_at DATETIME NOT NULL,
processed_at DATETIME NULL
);The UNIQUE constraint is important because checks at the application level can experience race conditions. Two requests that arrive almost simultaneously can both pass the check if the database does not help maintain uniqueness.
Quick response, heavy processing later
Once the signature and basic structure are valid, the endpoint should log the event and provide a success response as quickly as possible. Heavier work can be moved to a queue or background process.
The simple pattern is:
- Receive the request and read the raw body.
- Verify the signature.
- Validate important fields.
- Store the event in the database with a unique ID.
- Send an appropriate HTTP response.
- Process order changes, emails, or synchronization through a worker.
With this pattern, the webhook is less likely to timeout. However, do not return a success status if the event has not been stored or guaranteed to be processed. A success response should mean that the application has taken responsibility for that data.
How to handle failures?
Log every received event, including the time, event type, transaction ID, processing status, and safe error messages. Avoid storing secret keys or sensitive data carelessly in logs.
Provide an internal retry mechanism for temporary failures, such as when the database or external services are unavailable. Use exponential backoff to prevent the system from continuously flooding the troubled service.
For specific cases, provide a page or administrative command to reprocess failed events. This feature is much safer than asking the technical team to directly modify data in the production database.
What does this mean for us?
Webhooks can indeed make API integrations more efficient, but they also add entry points to the application. Therefore, webhook endpoints need to be treated as an important part of the system, not just a small PHP file that receives JSON.
A minimal checklist that can be applied now:
- Use HTTPS.
- Verify signatures using secrets stored in environment variables.
- Validate the structure and values of the payload.
- Store unique event IDs.
- Do not process heavy work before sending a response.
- Log failed events without leaking secret data.
- Test for duplicate requests, corrupted payloads, incorrect signatures, and timeouts.
Start with the most important webhook, such as payment notifications or order status changes. Once the flow is secure and easy to observe, the same pattern can be used for other integrations. The goal is not to make the code look complicated, but to ensure the system remains trustworthy when the real world starts sending data that does not always arrive on time and not always just once.
– Rio Yotto @rioyotto
