The shortest answer to "what is a webhook" is this: when an event happens in one system, that system automatically sends an HTTP request to a URL you define. So instead of constantly asking "did something new happen?", the other side calls you the moment the event occurs. This small inversion forms the backbone of modern integrations: payment providers, Git platforms, Discord, email services and nearly every SaaS product rely on webhooks.
The difference between polling and webhooks
In the polling model, your application repeatedly goes to the remote API and asks "has anything changed?" Most of the time the answer is "no", so the bulk of your requests are wasted. Even if you ask once a minute, you get an average 30-second delay between the event and learning about it.
With the webhook model you work event-driven: when something changes, the other side instantly pushes data to your endpoint. The advantages are clear:
- Real time: you hear about an event the instant it happens — latency is milliseconds, not seconds.
- Efficiency: instead of thousands of wasted requests, traffic is generated only for real events.
- Scalability: your server isn't worn out by needless queries, and you don't hit rate limits.
Polling still has its place: when the other side offers no webhooks, or when guaranteed delivery is a hard requirement. But if you have the choice, the event-driven approach is almost always cleaner.
What does a webhook request look like?
A webhook request is really just an ordinary HTTP POST. The sending system carries the event details, usually in a JSON body. A payment confirmation, for example, might look like this:
POST /webhooks/payment HTTP/1.1
Host: your-site.com
Content-Type: application/json
X-Signature: t=1719500000,v1=4a9f...c2
{
"event": "payment.succeeded",
"data": {
"id": "pay_8sK2",
"amount": 4900,
"currency": "USD",
"customer": "cus_12"
}
}
Your side receives this request, reads the body, does some work based on the event type (confirms the order, sends an email, grants the user a role) and quickly returns 200 OK to the sender. The critical point here: respond fast. Push heavy work (generating reports, calling external APIs) onto a queue; don't keep the webhook waiting.
Building a webhook receiver with Laravel
In practice, writing a webhook endpoint is very simple. With Laravel, a route and a controller are enough:
// routes/web.php
Route::post('/webhooks/payment', [WebhookController::class, 'handle'])
->withoutMiddleware([VerifyCsrfToken::class]);
Note: webhooks come from external systems and carry no session cookie, so you need to exclude CSRF protection from this route. In the controller we process the event:
public function handle(Request $request)
{
$payload = $request->all();
if ($payload['event'] === 'payment.succeeded') {
ProcessPayment::dispatch($payload['data']);
}
return response()->json(['ok' => true]);
}
Here we hand the work off to a queue job with ProcessPayment::dispatch(...), so the controller responds instantly and the heavy lifting runs in the background.
Signature verification: the heart of webhook security
Because your webhook URL is publicly reachable, you need to stop a malicious actor from sending fake requests. Most providers sign the body with a shared secret using HMAC and send the signature in a header (e.g. X-Signature). You recompute the signature with the same key and compare:
$signature = $request->header('X-Signature');
$expected = hash_hmac('sha256', $request->getContent(), $secret);
if (! hash_equals($expected, $signature)) {
abort(403, 'Invalid signature');
}
Critical detail: compute the signature over the raw body (getContent()), not the parsed array, because re-serializing the JSON can introduce byte-level differences. Also use hash_equals(), since == is vulnerable to timing attacks.
Reliability: retries and idempotency
In the real world things don't always go smoothly. If your server can't respond for a moment, most providers retry the request. That means the same event may arrive more than once. So your handlers must be idempotent:
- Every event has a unique
idfield; store it. - If the same
idarrives again, don't redo the work — just return200. - Return a fast
2xx; a slow response makes the provider treat you as failed and retry needlessly.
To test webhooks during development, you can expose your local server with a tunnel tool like ngrok and trigger test events from the provider's dashboard.
Frequently Asked Questions
What's the difference between a webhook and an API?
An API is usually a "pull" model where you initiate the request: you ask, you get an answer. A webhook is a "push" model: when an event happens, the other side sends data to you. Most integrations use both together.
How do I keep my webhook URL secure?
Always use HTTPS, verify the signature of every incoming request, allowlist the provider's IP range if possible, and keep the secret in an environment variable (.env) rather than in code.
What do I do if a webhook doesn't arrive?
First check the provider's delivery logs; most show failed attempts and the returned HTTP code. Make sure your endpoint really returns 200 by logging the raw body, and if needed, manually resend the event from the dashboard.
Need an event-driven integration? I can help you build secure webhook flows for payments, Discord, Git or your own systems. Get in touch and let's talk about your project.