What Happens When You Type a URL and Press Enter
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →Payment confirmations, Git pushes and form submissions often arrive as webhooks. How they work, how to verify signatures, handle retries and duplicates, and test them on your laptop.
Webhooks are how other services tell your app that something happened: a payment succeeded, a subscription was cancelled, someone pushed to a repository, a form was submitted. They're simple in concept and surprisingly easy to get wrong in ways that lose data or open security holes.
Instead of your app repeatedly asking "anything new?", the other service sends an HTTP POST to a URL you provide whenever an event happens:
POST /webhooks/payments
Content-Type: application/json
Webhook-Signature: t=1767225600,v1=5257a869e7ec...
{
"id": "evt_8f3a",
"type": "payment.succeeded",
"data": { "orderId": "991", "amount": 4999 }
}
Your endpoint receives it, processes it and responds with a 2xx status to say "got it."
Your webhook URL is public. Anyone who discovers it can send fake events, like a fake "payment succeeded." So every reputable provider signs its webhooks, usually with an HMAC of the raw request body using a secret only you and the provider know.
import crypto from 'node:crypto'
function isValidSignature(rawBody, signatureHeader, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(signatureHeader)
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
Two important details:
Many providers include a timestamp in the signature. Reject events that are too old, to prevent attackers replaying a captured request later.
Providers wait only a short time for your response. If your handler sends emails, calls other APIs and updates several tables before responding, it may time out, and the provider will retry, causing duplicate processing.
The reliable pattern:
200 immediately.Webhook delivery is usually at least once. Network hiccups and retries mean you'll sometimes receive the same event twice. Make processing idempotent:
const exists = await db.webhookEvents.findUnique({ where: { id: event.id } })
if (exists) return res.status(200).end() // already handled
await db.webhookEvents.create({ data: { id: event.id, type: event.type, payload: event } })
await queue.add('process_webhook', { eventId: event.id })
res.status(200).end()
A unique constraint on the event ID makes this safe even under concurrent deliveries.
Events can arrive out of order: subscription.updated before subscription.created, or two updates swapped. Options:
Don't return errors for events you simply don't care about. Acknowledge them with 200 and ignore them, or the provider will keep retrying and may eventually disable your endpoint.
Providers can't reach localhost. Options:
(Logging and monitoring basics.)
Payment webhooks deserve special care: they're often the only reliable signal that money moved, because users can close the browser before your success page loads. Treat the webhook as the source of truth for order fulfilment. (Adding payments to your app.)
An HTTP request that a service sends to your app's URL when an event happens, such as a successful payment, so your app can react without constantly polling for changes.
Verify the provider's signature using the raw request body and a shared secret, check the timestamp to prevent replays, use HTTPS and never trust the payload without verification.
Webhook delivery is usually at least once, so retries after timeouts or network errors can deliver duplicates. Store event IDs and skip events you've already processed.
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →There are dozens of HTTP status codes, but about fifteen cover nearly everything. What each one means, when to return it, and the mix-ups (401 vs 403, 301 vs 302) that confuse everyone.
Read →A, AAAA, CNAME, MX, TXT and NS records in plain English, why DNS changes seem slow, and the troubleshooting commands that tell you what is really going on.
Read →