Quick answer: Webhooks and callbacks are HTTP requests your service makes, not ones it receives, and they deserve the same contract discipline as inbound operations. Define their payloads, signing, and the expected receiver response in the spec, and you can generate mocks and validation for the outbound side instead of debugging integration failures by reading logs after a partner misses an event.
I treated webhooks as an afterthought for years, which is how we ended up emailing a new partner a sample JSON payload and a sentence saying "we sign it somehow." Their receiver rejected the first real event because a field we had changed during development was no longer in the sample, and the signature header name did not match what their verification expected. The inbound API was fully documented; the outbound contract existed only in a wiki page and a handful of hardcoded handlers. Moving it into the OpenAPI spec closed that gap.
Webhooks versus callbacks: know which one you mean
OpenAPI gives you two related constructs, and they model different relationships.
-
webhooks(a top-level element in OpenAPI 3.1) describes out-of-band calls the server may make at any time, keyed by an event name. They are not tied to a specific operation. -
callbacksare attached to an operation and describe a request the server makes as a consequence of that operation, often to a URL the client supplied at call time.
Use a webhook for something like "payment status changed" that can fire repeatedly and asynchronously. Use a callback when the client hands you a notification URL as part of a request, such as submitting a job and providing where to post the result.
Defining an event payload at the top level
A webhook entry reuses the same schema machinery as an operation, so the payload is a first-class, reusable component rather than a free-text example.
The receiver's response matters and is worth documenting. A 2xx acknowledges delivery; anything else is a retry. A 410 is a signal to permanently stop, which prevents a dead endpoint from being retried forever.
What the contract needs beyond the payload
The JSON body is only part of what a receiver has to verify. The table below lists the pieces that most often cause integration failures and why they belong in the spec.
| Element | Left undocumented | In the contract |
|---|---|---|
| Event payload | Sample drifts from reality | Reusable schema, versioned with the API |
| Signature header | Partners guess name and scheme | Header name and signing algorithm fixed |
| Event ID | Duplicate delivery undetectable | ID lets receivers deduplicate retries |
| Ack responses | Unclear what stops a retry | 2xx vs 410 behavior explicit |
| Delivery timing | Retries and order assumed | At-least-once and ordering stated |
Signing and the headers receivers depend on
Even when the body is correct, a receiver cannot trust it without a signature. Document the exact header and the verification steps, and include a timestamp to limit replay. A typical delivery looks like this.
Because the headers are named in the spec alongside the body schema, a partner can build verification against the real contract rather than an example, and you can generate a mock receiver during development that enforces the same checks.
Callbacks with a runtime URL
A callback's target is not known at design time, so the path uses a runtime expression that references the URL the client sent. OpenAPI expressions such as {$request.body#/notificationUrl} let you describe the outbound call even though its destination varies. That keeps the dynamic part explicit while still pinning the payload and the acknowledgment to a schema.
Generate the test scaffolding, not just the docs
Once the outbound calls are in the spec, the same tooling that serves inbound operations becomes available: a mock receiver that asserts every event matches its schema and signature, and a validator that fails a build if a handler emits a field not in the contract. That is the practical payoff. Webhook drift moves from a production incident found through a partner's complaint to a local failure the developer fixes before shipping.
FAQ
What is the difference between a webhook and a callback?
A webhook is a top-level, out-of-band event the server can emit at any time. A callback is tied to a specific operation and goes to a URL the client provided in that request. Both are outbound requests but have different lifecycles.
Why do partners keep rejecting our events?
Usually the payload they were given drifted from what you send, or the signature header and algorithm do not match. Document the body schema, header names, and signing steps together so verification is built against the real contract.
How should a receiver stop unwanted deliveries?
Return a 2xx to acknowledge and a 410 Gone to indicate the subscription is permanently removed. Documenting that response lets the sender stop retrying a dead endpoint instead of hammering it indefinitely.
Can the spec describe a callback when the URL is chosen at runtime?
Yes. Use a runtime expression referencing the client-supplied URL, such as the notificationUrl field, while keeping the payload and acknowledgment tied to fixed schemas.
This is part of the OpenAPI Guide — start there for the full picture of designing and documenting a complete HTTP contract.
Top comments (0)