Documenting webhooks in OpenAPI 3.1, and the half it cannot describe
3.1 finally gave webhooks a home in the spec. It describes the payload, which is the easy part, and says nothing about delivery, which is where integrations break.

Before 3.1, webhooks had no natural place in an OpenAPI document. Teams either bolted them into paths as endpoints they did not serve, or documented them in prose beside the spec and let them drift.
3.1 added a top-level webhooks keyword, and it is a genuine improvement. It is also commonly mistaken for solving the whole problem, when it solves the smaller half.
What the keyword does
webhooks sits at the root, beside paths, and describes requests your API sends rather than requests it receives:
openapi: 3.1.0
webhooks:
invoicePaid:
post:
summary: Sent when an invoice moves to paid.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/InvoicePaidEvent'
responses:
'2XX':
description: Acknowledged. Any 2xx stops redelivery.The inversion is the point. The requestBody is what you send to the consumer, and the responses are what you expect back from them. Reading it as an endpoint you serve gets the direction backwards.
The immediate payoff is that consumers can generate types for your event payloads, which is the thing they most want and most often hand-write from a documentation table.
Webhooks and callbacks are not the same thing
Both exist, they look similar, and the distinction is worth getting right because it changes where the definition goes.
Callbacks are attached to an operation. They describe a request your API makes as a consequence of that specific call, to a URL supplied in that call. A job submission that takes a callbackUrl and posts back when the job finishes is a callback.
Webhooks are not attached to any operation. They are events your API emits based on things happening in your system, to a URL configured out of band, usually in a dashboard.
If the destination came from a field in a request, it is a callback. If it came from settings, it is a webhook.
Design the event envelope once
The most useful decision is one the spec will not prompt you to make: give every event the same outer shape, with the type and the payload separated.
{
"id": "evt_01HQ...",
"type": "invoice.paid",
"created": "2026-10-29T09:12:04Z",
"data": { }
}That envelope is what lets a consumer write one handler that switches on type, and it is what lets you add event types without every consumer changing their parsing. Four fields earn their place:
id, because deduplication needs a stable key and this is it. type, dotted and stable, because it is the thing consumers branch on and renaming one breaks every handler. created, because out-of-order delivery is normal and consumers need to know which of two events is newer. data, because keeping the payload in its own object is what stops a new envelope field colliding with a payload field.
The half OpenAPI does not describe
Here is the part that decides whether an integration works, and none of it is expressible in the spec. It belongs in a written guide, and it is the guide most teams do not have.
Signature verification. Which header carries the signature, what exactly is signed (the raw body, and whether a timestamp is included), which algorithm, and how to rotate a secret. The critical detail people get wrong: consumers must verify against the raw body, before any JSON parsing, because re-serialising changes the bytes and the signature will not match. If your framework parses the body before your handler sees it, say so, because that is the single most common integration failure.
Retry schedule. How many attempts, over what window, with what backoff. What counts as success, which should be any 2xx and nothing else. Whether a 410 stops redelivery permanently.
Ordering. Almost certainly not guaranteed. Say it plainly, because consumers assume ordering by default and build state machines that depend on it.
Delivery semantics. At-least-once, which means duplicates. Combined with the id field, that gives consumers what they need to be idempotent, and you should show that pattern rather than describing it.
Timeout. How long you wait for their acknowledgement before treating it as failed. Consumers need this to decide whether to process inline or enqueue and return immediately. The right advice is almost always enqueue and return.
IP ranges or a static egress, if you have one, because some consumers need it for their firewall.
Testing is part of the contract
A webhook integration cannot be developed without a way to trigger events on demand. If your dashboard has no "send a test event" button, every consumer builds a workaround, usually by making real objects in your system to force real events.
Alongside it: a delivery log showing what you sent, what came back and when you retried. This is what turns "your webhooks are not working" into a resolvable conversation, and its absence turns every delivery question into a support ticket that only you can answer.
Where this leaves the document
Put the event schemas in webhooks. Consumers get generated types and your reference page gets a section it did not have.
Then write the delivery guide, because the schema tells someone what the JSON looks like and the guide tells them how to build a handler that is correct under retries, duplicates and reordering. The first is generated. The second is the kind of page that has to be written, alongside your errors page and your auth flow.
What belongs in API docs that OpenAPI cannot generate covers the rest of that set and how to decide what goes in each. If your spec is not on 3.1 yet, the spec audit will tell you what else is holding your generated output back, free and without an account.



