A webhook endpoint receives events over HTTP when another system has something to report. That makes it useful for payment notifications, order updates, and integrations—but it also puts an internet-facing handler on the critical path of your application.
A reliable receiver has two separate jobs: establish that the message was authenticated, and process the event without losing it or applying it twice. A valid signature alone does not solve retries, replay, ordering, or storage failures.
Start with the sender's contract
Before writing a handler, find out:
- Which request method, headers, body, and encoding does the sender use?
- Exactly which bytes are signed, and is a timestamp included in the signed material?
- How are keys, signature versions, and key rotation handled?
- Which response codes count as success, and when will the sender retry?
- Are event identifiers stable across retries, and is ordering guaranteed?
Do not infer one provider's behavior from another. Some use a shared HMAC secret, others use asymmetric signing, and their header formats can differ.
Your endpoint should use HTTPS, enforce reasonable method and body-size limits, preserve the raw body, authenticate the message, validate its structure, and durably record an event before acknowledging it. For a general overview of HTTP integration design, those responsibilities matter independently of your framework.
Why the raw body matters
An HMAC uses a secret key and an exact byte sequence to compute an authentication code. If the recipient uses the same key and signed bytes, it can compute the expected code and compare it with the supplied one.
Parsing JSON and serializing it again before verification can change whitespace, escaping, or field order. Even when the resulting object looks identical, the bytes can differ and the signature will fail. Capture the raw input, verify it according to the sender's protocol, and only then parse the JSON.
HMAC authenticates the signed material; it does not encrypt the payload and it is not an asymmetric digital signature. HTTPS protects the connection. Verified payload fields still need schema validation, authorization checks, and safe handling when used in a database or rendered in a page.
A deliberately fictional signature format
For a concrete example, suppose both sides agree on this protocol:
-
X-Hook-Timestampis a ten-digit Unix timestamp in seconds. - The signed message is the timestamp, a dot, and the exact raw body.
-
X-Hook-Signatureis a lowercase hexadecimal HMAC-SHA256. - The receiver accepts a five-minute clock window.
These names and rules are invented for this example. They are not instructions for a specific vendor.
<?php
declare(strict_types=1);
function verifyExampleWebhook(
string $rawBody,
string $timestamp,
string $signature,
string $secret,
int $now
): bool {
if ($secret === '') {
return false;
}
if (!preg_match('/\\A[0-9]{10}\\z/', $timestamp) ||
!preg_match('/\\A[0-9a-f]{64}\\z/', $signature)) {
return false;
}
$sentAt = (int) $timestamp;
if ($sentAt < $now - 300 || $sentAt > $now + 300) {
return false;
}
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $signature);
}
~~~{% endraw %}
Here the timestamp is included in the authenticated bytes; an unsigned timestamp does not prevent an attacker from changing it. The server clock needs to be synchronized. The function accepts a secret provided by protected configuration and fails closed if it is empty.
This is a verification function, not a complete endpoint. It does not read the input, store an event, or send an HTTP response. Adapt the actual algorithm, signed bytes, encoding, allowed clock window, and handling of multiple keys to the sender's documented protocol.
## Bound the input and validate the event
At the HTTP server and application layers, constrain request size and execution time. In PHP, a handler commonly reads raw bytes from {% raw %}`php://input`. Read no more than your configured limit plus one byte and reject an oversized request based on bytes actually read, not solely on a client-provided content length.
If the body cannot be read, the secret is missing, or verification fails, do not execute business logic. Once verification succeeds, decode the payload with error handling and check field types, event type, account identity, and environment. Do not insert incoming values into SQL, HTML, shell commands, or URLs without the appropriate validation and context-specific protections.
For sensitive state changes, consider retrieving the current object through the provider's authenticated API. An authenticated notification can be stale or arrive after a newer event.
## Design for duplicates and replay
Senders can retry a legitimate event when their first request times out. An authenticated request can also be replayed. A signed timestamp limits the time window, but an identical request can still be repeated within it.
After verification, use a stable event identifier that is included in authenticated data or otherwise securely bound to the message. Scope the identifier to the provider and environment where necessary. Enforce uniqueness in durable storage; a read-then-insert check by itself can race when two deliveries arrive simultaneously.
A robust pattern is to insert the received event and a work item into the same database transaction. If you publish to an external broker, a transactional outbox or an equivalent proven delivery mechanism can bridge the database and broker. On a uniqueness conflict, do not repeat the business operation; respond according to the sender's contract.
A database transaction on your side cannot make an external API call atomic. For external effects, use the downstream service's idempotency mechanism if available, or build reconciliation appropriate to the business operation. Choose retention for event identifiers with the provider's retry behavior and your risk profile in mind.
## Acknowledge only after durable acceptance
Long synchronous work can cause the sender to time out and retry. A common design is to verify the request, validate the minimal event envelope, write the event and job durably, commit, and then return the success response required by the sender. The worker can perform slower tasks later.
If the durable write fails, do not report success. Determine the appropriate failure response from the sender's retry contract. Workers should tolerate being run more than once: a process can complete an external side effect and crash before recording its result locally.
Do not assume delivery order. If the protocol provides trustworthy versions or modification times, use them to avoid applying stale state. When ordering remains uncertain, reconcile with the provider's authoritative state.
## Rotate keys and keep logs useful
Use separate secrets for production and testing, and for independent integrations. Keep them out of source control, URLs, frontend code, and logs. During a planned rotation, accept old and new keys only as the sender's protocol permits; remove the old key after the transition.
Record an event identifier, type, receipt time, verification outcome, processing state, and retry count. Avoid logging full sensitive payloads or signatures unless there is a justified, protected need. Limit log access and retention. Rate limiting and an up-to-date provider IP allowlist, when feasible, are defense-in-depth rather than substitutes for signature verification.
## Test the failure paths
Before enabling production delivery, test at least:
- A valid message, a modified body byte, an altered signature, and missing headers.
- A stale and a future signed timestamp, plus reasonable clock drift.
- An oversized body, invalid JSON, missing secret, and unknown event type.
- Concurrent deliveries with the same event ID.
- A database failure before commit and a worker crash after commit.
- Retried and out-of-order events, key rotation, and production/test separation.
Test with the provider's real test events and retry behavior, not only with a hand-written mock. The desired result is a receiver that authenticates the message, records it safely, and handles repeated delivery without duplicating the effect.
Russian original: https://morlix.ru/blog/vebkhuki-kak-prinimat-sobytiya-i-proveryat-podpis
---
[Original article (Russian)](https://morlix.ru/blog/vebkhuki-kak-prinimat-sobytiya-i-proveryat-podpis)
Top comments (0)