A webhook is an HTTP request sent by one application to notify another application that an event occurred. Instead of repeatedly asking whether a payment succeeded, your server exposes an endpoint and the payment provider sends an event when the status changes.
Webhooks look simple but production reliability requires signature verification, idempotency, queues, retries, observability and protection against replay attacks.
Webhook vs API vs polling
| Method | Who starts the request? | Best use |
|---|---|---|
| Normal API call | The client that needs data or an action | Immediate request and response |
| Polling | The receiving application repeatedly | Systems without event delivery |
| Webhook | The event-producing application | Near-real-time event notification |
A webhook is still an API request. The difference is the direction and timing. The provider calls your endpoint when something changes.
How a webhook works
- You register an HTTPS endpoint with the provider.
- An event occurs, such as
payment.completed. - The provider serializes an event payload.
- It signs the raw payload with a shared secret or private key.
- The provider sends an HTTP POST request to your endpoint.
- Your server verifies authenticity and freshness.
- Your server stores the event and returns a success response quickly.
- A queue worker processes the business action.
- Failed deliveries are retried according to the provider’s policy.
Example webhook payload
{
"id": "evt_019abc",
"type": "payment.completed",
"created_at": "2026-08-10T18:30:00Z",
"data": {
"payment_id": "pay_019xyz",
"amount": 4900,
"currency": "USD"
}
}
The event ID identifies the delivery. The object ID identifies the business resource. Keep both, because several events can refer to one payment.
Never trust a webhook because it knows the URL
Webhook endpoints are public by design. Anyone who discovers the URL can send a request. Verify a cryptographic signature using the exact process documented by the provider.
A common HMAC design signs the timestamp and raw request body:
signature = HMAC_SHA256(secret, timestamp + "." + raw_body)
- Read the raw body without reformatting JSON.
- Extract the provider timestamp and signature headers.
- Reject timestamps outside a short tolerance.
- Compute the expected signature using the correct secret.
- Compare signatures with a constant-time function.
- Reject the request before processing if verification fails.
Do not invent a generic verifier when the provider supplies an official SDK. Signature formats, versioning and multiple active secrets vary.
Why the raw body matters
Parsing JSON and encoding it again can change whitespace, key order or number formatting. The provider signed the original bytes, so verify those bytes. Middleware that mutates the body before verification can cause valid requests to fail or create unsafe workarounds.
Idempotency prevents duplicate processing
Webhook delivery is normally at least once, not exactly once. A provider may retry because your response was slow or lost even though you processed the event. Store the unique event ID with a database uniqueness constraint.
// Pseudocode
begin transaction
insert webhook_event(id, type, payload)
// A unique constraint makes a repeated ID harmless.
commit
dispatch background job
return HTTP 200
If the insert reports a duplicate, return success without applying the business action again. Do not depend on an in-memory cache for financial idempotency.
Return quickly and process asynchronously
The endpoint should authenticate, validate minimally, persist and acknowledge. Slow external API calls, email, inventory updates and complex calculations belong in a queue worker.
This design reduces provider timeouts and protects delivery throughput. A successful HTTP response should mean “the event was safely accepted,” not necessarily “every downstream task finished.”
Events can arrive out of order
Do not assume delivery order. A retry of an older event may arrive after a newer one. When state matters, compare event versions or fetch the current resource from the provider’s API after receiving the notification.
For example, receiving subscription.updated after subscription.cancelled should not reactivate a cancelled subscription merely because the update arrived last.
Replay attack protection
An attacker who captures a valid request may send it again. Idempotency limits repeated business effects, while timestamp validation rejects valid signatures that are too old. Use both.
Rotate secrets according to the provider’s process and support an overlap window when two secrets can be valid. Store secrets outside source code and never log them.
Laravel webhook endpoint example
Generate the application pieces through Laravel Sail:
./vendor/bin/sail artisan make:controller WebhookController --invokable
./vendor/bin/sail artisan make:model WebhookEvent -m
./vendor/bin/sail artisan make:job ProcessWebhookEvent
./vendor/bin/sail artisan test --filter=Webhook
A simplified controller structure looks like this:
public function __invoke(Request $request): Response
{
$rawBody = $request->getContent();
// Use the provider's official verifier where available.
$event = $this->verifier->verify(
rawBody: $rawBody,
signature: (string) $request->header('Webhook-Signature'),
timestamp: (string) $request->header('Webhook-Timestamp'),
);
$stored = WebhookEvent::query()->firstOrCreate(
['provider_event_id' => $event->id],
['type' => $event->type, 'payload' => $rawBody]
);
if ($stored->wasRecentlyCreated) {
ProcessWebhookEvent::dispatch($stored->id);
}
return response()->noContent();
}
The production implementation also needs a maximum body size, provider-specific error handling, encrypted or minimized payload storage, secret management and authorization inside the job.
Do not disable CSRF protection globally
External providers cannot supply your application’s browser CSRF token. Place webhook routes in an appropriate stateless API context or exclude only the exact endpoint according to the framework version. Signature verification replaces origin authenticity for that endpoint. It does not justify disabling CSRF across the application.
Outgoing webhooks
If your application sends webhooks, use a transactional outbox. Commit the business change and an outgoing event record in one database transaction. A worker signs and delivers the event later.
- Give every event a stable unique ID.
- Sign the timestamp and raw body.
- Use HTTPS and reject unsafe destination URLs.
- Retry temporary failures with exponential backoff and jitter.
- Stop retrying permanent 4xx responses unless your contract says otherwise.
- Provide delivery logs and manual replay.
- Preserve the original event ID during replay.
- Set connection and response timeouts.
Prevent SSRF in user-configured webhook URLs
A webhook feature that lets users enter any destination can become a server-side request forgery path. An attacker may target local services, cloud metadata endpoints or private networks.
- Allow HTTPS only.
- Resolve and block loopback, link-local, private and reserved IP ranges.
- Revalidate after redirects and DNS resolution.
- Limit redirect count and destination ports.
- Apply egress firewall rules.
- Do not attach internal credentials to webhook requests.
- Set strict body, connection and response limits.
Testing webhooks locally
- Use the provider’s official CLI or test-delivery feature when available.
- Expose the local endpoint through a trusted temporary tunnel.
- Configure a separate test secret.
- Record the raw test payload and headers without secrets.
- Test valid, invalid, old and malformed signatures.
- Send the same event several times.
- Simulate timeouts and server errors.
- Verify that jobs, retries and dead-letter handling behave correctly.
Do not use a production secret or customer payload in a third-party request inspector unless its privacy terms and retention are acceptable.
Observability checklist
- Provider, event ID and event type.
- Received, verified, queued, processed and failed timestamps.
- HTTP delivery attempt and response category.
- Processing duration and queue delay.
- Duplicate-event count.
- Signature failures without logging the secret.
- Retry count and final failure reason.
- Correlation ID linking the event to business records.
Alert on sustained failure rate, growing queue delay and repeated signature errors. Do not page a developer for every individual retry.
Common webhook mistakes
- Trusting an IP allowlist instead of verifying signatures.
- Parsing JSON before signature verification.
- Performing all business work before responding.
- Assuming exactly-once or ordered delivery.
- Using the object ID as the event ID.
- Returning an error after safely storing an event, causing unnecessary retries.
- Logging full sensitive payloads.
- Disabling CSRF protection for the entire site.
- Accepting unrestricted destination URLs for outgoing webhooks.
- Having no replay or dead-letter process.
Webhooks and offline synchronization share the same distributed-systems realities: requests can be delayed, duplicated or delivered after the sender loses the response. Our offline-first architecture guide explains the related idempotency and conflict principles.
Frequently asked questions
Should a webhook return 200 before processing?
Return success after the event has been authenticated and durably accepted. Process slow downstream work in a queue. Do not acknowledge an event you could still lose.
Can webhooks be GET requests?
They can, but POST is standard for event payloads. Follow the provider’s documented contract and never place secrets in a query string.
What status should duplicate events return?
After verifying the signature, return a success status if the event was already accepted. The provider should not keep retrying a harmless duplicate.
The bottom line
A production webhook is a secure event-ingestion pipeline, not just a public controller. Verify the raw request, reject replays, store events idempotently, respond quickly, process asynchronously and make failures visible. Design for duplicate and out-of-order delivery from the beginning, because real networks will eventually produce both.
Discover more from TheTechTower
Subscribe to get the latest posts sent to your email.
