Webhooks & OpenAPI
Verify every webhook signature. Why OpenAPI is documentation that compiles.
Two sides of being a good API citizen
An API rarely lives alone. It is consumed by other systems, and it consumes others in turn. Two recurring problems come out of that, and this module takes one each.
The first: how does your API tell another system that something happened? A payment cleared, an order shipped, a document finished processing. The consumer needs to know — promptly — and you do not want them hammering your API asking "done yet? done yet?" The answer is webhooks.
The second: how does another developer learn to use your API without guesswork? Which endpoints exist, what each one takes, what it returns, what the errors mean. Passed around as prose and stale wiki pages, this is a constant source of friction. The answer is OpenAPI — a precise, machine-readable description of your API.
One is about your API pushing events outward. The other is about your API describing itself clearly. Both are what separate an API that is pleasant to integrate with from one that is a slog. The first three sections cover webhooks; the last three cover OpenAPI.
Webhooks — an API that calls you
Recall the limitation from the real-time module: in ordinary request/response, the client always speaks first, so a server cannot announce news on its own. The same problem exists between servers. Your system needs to know when Stripe processes a payment — but Stripe cannot reach into your system.
The crude fix is polling: your server asks Stripe's API "any new payments?" every minute, forever. It is wasteful — almost every poll finds nothing — and it is laggy, because news waits for the next poll.
A webhook inverts it. Instead of you repeatedly asking, you tell the provider once: "here is a URL of mine; whenever a payment clears, send an HTTP request to it." Now when the event happens, the provider's server makes an HTTP request to your server, carrying the event. No polling, near-instant delivery.
POLLING — you keep asking
your server ──"new payments?"──► Stripe "no"
your server ──"new payments?"──► Stripe "no"
your server ──"new payments?"──► Stripe "yes" (1 min late)
WEBHOOK — they call you when it happens
(once) your server: "events → POST https://api.mine.com/hooks/stripe"
(event) Stripe ──POST event──► your server ◄── instant
The mental flip worth holding: a webhook is just an API request where the roles are swapped. Normally you are the client calling someone's API. With a webhook, you implement an endpoint and the provider becomes the client calling yours. Everything you know about handling HTTP requests still applies — you are now on the receiving side of one.
// You expose this endpoint; the provider POSTs events to it.
app.post('/hooks/stripe', async (req, res) => {
const event = req.body;
if (event.type === 'payment_intent.succeeded') {
await orderService.markPaid(event.data.object.id);
}
res.status(200).send('ok'); // 2xx = "received" — see section 3
});
That endpoint looks simple, and the happy path is. The next section is about everything that makes a production webhook receiver harder than it looks — because a webhook is an untrusted request from the public internet, and it can arrive twice, out of order, or not at all.
Receiving webhooks safely — the hard parts
A naive receiver — take the body, trust it, act on it — is wrong in several ways at once. Four things have to be handled.
Verify it is genuine. Your webhook URL is reachable by anyone. Without verification, an attacker can POST a fake payment_intent.succeeded to it and your code marks an unpaid order as paid. So providers sign every webhook: they include a cryptographic signature header, computed from the request body and a shared secret only you and the provider know. Your receiver recomputes that signature and rejects the request if it does not match.
app.post('/hooks/stripe', (req, res) => {
const signature = req.headers['stripe-signature'];
if (!isValidSignature(req.rawBody, signature, WEBHOOK_SECRET)) {
return res.status(401).send('bad signature'); // forged — reject
}
// ...only now trust the payload
});
Signature verification is not optional. An unverified webhook endpoint is a hole through which anyone can inject events into your system.
Expect duplicates — be idempotent. Webhooks are delivered "at least once." Network hiccups and retries (next point) mean the same event can legitimately arrive two or three times. If your handler is not careful, one payment gets recorded twice. The fix is idempotency: every event carries a unique ID, you record which IDs you have processed, and a repeat is recognised and skipped.
if (await alreadyProcessed(event.id)) {
return res.status(200).send('duplicate ignored'); // safe no-op
}
await processEvent(event);
await markProcessed(event.id);
Designing the handler so that processing the same event twice has the same result as processing it once is the single most important property of a correct receiver.
Respond fast — acknowledge, then work. Providers expect a 2xx response quickly — often within a few seconds — and they read your status code as "did it arrive?" If your handler does slow work (calls other services, sends email) before responding, you risk timing out, the provider assumes failure and retries, and now you are processing duplicates you caused yourself. The pattern: validate, enqueue the event onto a job queue (Module 16), respond 200 immediately. The slow work happens in the background worker.
receive ─► verify signature ─► dedupe check ─► enqueue job ─► 200 OK
│
▼
background worker does
the slow processing
Understand the retry contract. A non-2xx (or a timeout) tells the provider "not received," and it will retry — typically several times with growing gaps (exponential backoff). This is a safety net: if your service is briefly down, events are not lost. But it is also why duplicates happen, and why a slow handler is dangerous. Return 2xx only when you have safely accepted the event (enqueued it counts); return non-2xx only when you genuinely want a retry.
OpenAPI — an API that describes itself
Switch sides. Now it is your HTTP API, and other developers need to use it. They have to know every endpoint, every parameter, every response shape, every error. Communicate that as prose — a wiki, a README, a Slack message — and it is verbose, easy to get subtly wrong, and always drifting out of date as the code changes.
OpenAPI is the fix: a standard, structured format for describing an HTTP API in a single document (YAML or JSON) that is precise enough for machines to read, not just humans. "OpenAPI" is the specification; "Swagger" is the older name for the same thing plus the popular tooling around it — you will see both words for one concept.
An OpenAPI document states, formally: every path, every method, every parameter with its type and whether it is required, every request and response body shape, every status code.
openapi: 3.0.0
info:
title: Orders API
version: 1.0.0
paths:
/orders/{id}:
get:
summary: Fetch a single order
parameters:
- name: id
in: path
required: true
schema: { type: string }
responses:
'200':
description: The order
content:
application/json:
schema:
type: object
properties:
id: { type: string }
total: { type: number }
status: { type: string }
'404':
description: No order with that id
That is dense, but notice what it is: a single, unambiguous source of truth for one endpoint. There is no room for "I think it returns a string?" — the contract is written down in a form that cannot be vague.
What the OpenAPI document buys you
A description format would be a chore if it were only a description. OpenAPI is worth the effort because that one document feeds a whole set of tools — you write the contract once and get many things from it.
Interactive documentation, generated. Tools like Swagger UI and Redoc turn the document into a browsable API reference — every endpoint listed, every shape shown, often with a "try it" button that makes real calls. You maintain the spec; the documentation site regenerates itself. No hand-written API docs to drift.
Client SDKs, generated. A generator can read the document and emit a typed client library — in TypeScript, Python, Go, whatever — so a consumer of your API gets real functions and real types instead of hand-rolling HTTP calls and guessing at JSON shapes.
Server stubs and request validation. The document can scaffold server-side route definitions, and — more usefully day to day — validate incoming requests against the spec automatically: a request that does not match the declared shape is rejected before your handler ever runs.
Contract testing. Because the document is a precise contract, you can test that your running API actually conforms to it — catching the moment code and contract drift apart.
There is a genuine choice in how the document and the code relate, and it is worth deciding deliberately:
Spec-first — write the OpenAPI document before the code. The spec is the design artifact and the agreed contract; the implementation is built to satisfy it. This is strong when multiple teams or external partners need to agree on the API before anyone builds it — frontend and backend can work in parallel against a contract both signed off.
Code-first — write the code, annotate it, and generate the OpenAPI document from those annotations. The document stays in lockstep with the code because it is produced from it. This is convenient for a single team moving fast on an API they fully control.
Neither is universally right. The failure mode to avoid is neither — an API with no machine-readable description at all, documented in prose that quietly goes stale, where every consumer learns the API by trial, error, and asking in chat.
A closing note on proportion, because OpenAPI is not free. A formal OpenAPI document is clearly worth it for a public API, an API consumed by partners or other teams, or any API stable and important enough that a precise contract prevents real misunderstanding. It is reasonable to skip for a tiny internal API consumed only by one frontend the same team owns, or a throwaway prototype — there, the ceremony can cost more than the drift it prevents. As with the rest of this module: match the rigor to the stakes. A widely-consumed API deserves both halves of good citizenship — webhooks done safely so it pushes events well, and OpenAPI so it describes itself honestly. That is what makes an API something other people are glad to build on. This closes the integration topics; the next modules move into operating an API at scale.
The Backend from First Principles series is based on what I learnt from Sriniously's YouTube playlist — a thoughtful, framework-agnostic walk through backend engineering. If this material helped you, please go check the original out: youtube.com/@Sriniously. The notes here are my own restatement for revisiting later.