How to Document Webhooks (With a Real Example)
What should a webhook documentation page actually contain?
A useful webhook doc has six things in one place: the list of event types you emit, the exact JSON payload for each event (including data types and nullability), how signature verification works with a working code sample, your retry and timeout behavior, your guarantees around idempotency and event ordering, and instructions for testing and replaying events. Skip any one of these and integrators will file a support ticket within the week.
Most webhook docs you read only get three of those right. The rest are footnotes buried in a changelog or a forum post. Below is a concrete example you can copy, built around a payment.succeeded event, followed by the checklist for every event page in your docs.
What a good webhook page looks like
Here is the payload for a single event. Notice the comments: every field has an explicit type, a description, and the word "nullable" or an example where the value isn't obvious.
{
"id": "evt_1Nx3kLZyz8aBcDeFgHiJkLmN",
"type": "payment.succeeded",
"created": 1714000000,
"api_version": "2026-04-01",
"livemode": true,
"data": {
"object": {
"id": "pay_01HV9Q2X8R",
"amount": 2499,
"currency": "usd",
"customer": "cus_9Kx2aBcDeF",
"payment_method": "pm_1Nx3k...",
"status": "succeeded",
"metadata": {
"order_id": "ord_7821"
},
"receipt_url": "https://example.com/r/abc123"
},
"previous_attributes": null
}
}
That payload is only half the page. The other half is the behavior your doc has to spell out explicitly, because integrators cannot infer it from the JSON alone.
Signature verification. Every webhook request carries a header like X-Signature: t=1714000000,v1=5257a869e.... Show the exact verification algorithm, not a vague reference to HMAC. A working pseudocode block looks like this:
signing_secret = "whsec_xxx" # from your dashboard
header = request.headers["X-Signature"]
timestamp, signature = parse(header)
# Reject replays older than 5 minutes
if abs(now() - timestamp) > 300:
reject()
signed_payload = timestamp + "." + request.body # raw bytes, not parsed JSON
expected = hmac_sha256(signing_secret, signed_payload)
if not constant_time_compare(expected, signature):
reject()
Three things that page needs to say out loud: verify against the raw request body before JSON parsing, reject timestamps outside a tolerance window to prevent replay, and use a constant-time comparison. Integrators will get this wrong if you don't show it.
Retries and timeouts. State the numbers. "We retry failed deliveries" is useless. "We retry up to 16 times over 3 days with exponential backoff starting at 10 seconds. Your endpoint has 15 seconds to respond with a 2xx before we mark the delivery as failed" is a spec. Include what counts as a failure (non-2xx, timeout, connection reset) and what counts as success (any 2xx, body ignored).
Idempotency and ordering. Tell readers whether event.id is safe to use as a dedupe key, and commit to it. Tell them whether events can arrive out of order, and if so, that they should use created or a resource version number rather than arrival order to reconcile state. If a resource can emit created and updated events within the same second, say so.
Event list. A single table at the top of the docs, alphabetized, linking each event name to its dedicated page. Include a one-sentence description and a "when this fires" column. Readers scan this table to decide which events they need to subscribe to, so every row earns its keep.
Testing and replay. Link to the dashboard page where integrators can send test events, list the CLI command that forwards events to localhost, and document how to replay a specific evt_... by ID. Stripe and Shopify both do this well; almost everyone else makes you reverse-engineer it from the dashboard.
The per-event page template
Every individual event page (one per event type) should contain the same sections in the same order. Consistency here matters more than prose: developers skim these pages with Cmd+F, and they want to find the payload without reading.
- Event name and a one-sentence description.
payment.succeededfires when a charge completes and funds are captured. - When it fires, with edge cases. Include the async path ("fires on capture, which may be hours after authorization for delayed-capture flows") and anything that would surprise the reader.
- Full example payload in JSON, realistic values, not
"string"placeholders. - Field reference. A table: field path, type, nullable, description, example.
- Related events. Cross-link to
payment.failed,payment.refunded, any event that shares the same resource. - Common integration patterns. Two or three typical "on this event, do X" handlers, with code.
That is the entire spec. If you follow it for every event, your webhook docs will be better than 80% of what ships in production. The moat isn't the prose. It is the discipline of filling in every section for every event, instead of handwaving on the obscure ones.
Tools that generate docs from your code (we built Docsio partly for this) can scaffold the structure from a schema or an OpenAPI webhook definition, but you still have to write the "when it fires" and "common patterns" sections yourself. Those are the parts that require someone who has actually integrated the API to sit down and type.
What to do next
If you are starting from scratch, write the event list table first, then fill in one event page end to end before you touch the others. It is easier to copy a good template eight times than to write eight half-finished ones in parallel.
Two adjacent reads: API reference documentation covers the endpoint-shaped version of the same discipline, and OpenAPI documentation explains how to describe webhook events formally in the 3.1 spec so your docs can be generated from the schema. If you are auditing a full API surface, API documentation best practices is the checklist to run alongside this one.
Webhooks are where integrations break in production and where good docs pay for themselves within a week. Write them like someone is going to copy the JSON straight into a unit test, because that is exactly what is going to happen.
