Webhooks are first-class in OpenAPI 3.1. In 3.0 you had to shoehorn them under callbacks:, which only made sense if the webhook was triggered by an inbound request. Modeling "we send your endpoint a webhook whenever X happens" cleanly required a workaround.
3.1 added a top-level webhooks: block that's symmetric with paths:. This is the short reference for using it well.
The basic shape
openapi: 3.1.0
info:
title: Acme Messaging API
version: 1.4.0
paths:
/messages:
post:
operationId: sendMessage
...
webhooks:
messageReceived:
post:
operationId: messageReceivedWebhook
summary: Sent when an inbound message arrives.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/InboundMessage'
example:
id: "msg_01abc"
from: "+15551234567"
to: "+15557654321"
content: "Reply"
received_at: "2026-05-10T14:23:00Z"
responses:
"200":
description: Acknowledged.
"410":
description: Unsubscribe — the endpoint is gone permanently.
components:
schemas:
InboundMessage:
type: object
required: [id, from, to, content, received_at]
properties:
id: { type: string }
from: { type: string }
to: { type: string }
content: { type: string }
received_at: { type: string, format: date-time }
Each entry under webhooks: is a named webhook with the same operation structure as a path. The post: describes what you send to the customer's endpoint, with requestBody: defining the payload and responses: defining what the customer's endpoint should return.
What good docs say about each webhook
Generated docs should answer:
- When does the webhook fire? State the trigger explicitly. "Whenever a message is received from a number not in the contact list" is better than "on inbound."
- What's in the payload? The schema covers shape; the description should cover semantics. What does
frommean for international numbers? Isreceived_atUTC or local? - Retry policy. If the customer's endpoint returns 5xx, do you retry? With what backoff? For how long?
- Signature verification. How does the customer verify the webhook came from you? An
X-Signatureheader with an HMAC of the body is the standard pattern. Document the algorithm, the secret source, and the canonical body to sign. - Idempotency. What identifier on the payload can the customer use to deduplicate? (Usually the
idfield.) Webhooks are at-least-once; duplicates happen. - Disabling. What does the customer's endpoint return to stop receiving the webhook? (
410 Goneis the convention.)
Webhooks vs callbacks vs SSE
Webhooks (3.1 top-level), callbacks (under operations), and server-sent events solve different problems:
- Webhooks (3.1) — out-of-band push to a customer-provided endpoint. The customer registers a URL once. Use for events that happen without a triggering request from the customer.
- Callbacks — push triggered by a specific incoming request, where the customer provided a callback URL in the request body. Use for async response patterns. Still useful in 3.1 for this narrower case.
- SSE / streaming — long-lived HTTP connection from the customer to you, server pushes events over the connection. Different deployment shape: the customer initiates, you stream.
If you're modeling something that looks like webhooks, use webhooks:. Don't reach for callbacks just because you remember them from 3.0.
Signature verification — the spec lets you describe it, but the SDK has to implement it
A common gap: the spec models the webhook payload, but the signature verification is documented in prose only. Generated SDKs can't help the customer verify because the spec doesn't say how.
Two ways to close the gap:
- Define a signature header in your auth model. Add a
securitySchemefor the signature header and reference it on the webhook operation. Generated docs will surface it. - Ship a signature-verification helper in your SDK alongside the generated code. Most teams maintain this hand-written. Bloom-generated SDKs don't auto-include this today; on the roadmap.
In either case, document the algorithm (typically HMAC-SHA256 of the raw body), the secret source (a per-customer key from your dashboard), and the canonical input.
Versioning webhooks
Webhooks evolve like any other API surface. The two strategies that work in practice:
- Versioned event types.
message.received.v1vsmessage.received.v2. New shapes get new types; customers explicitly subscribe to the new version. - Versioned endpoints. Customer registers
/v1/inboundvs/v2/inbound. You send the version-matched payload to each.
Both work; the first is more flexible for fine-grained event-level changes, the second is simpler. Don't quietly evolve a payload — customer parsers break in production.
What Bloom does with webhooks:
Bloom's SDK generator reads webhooks: and emits:
- Typed event payload interfaces (TypeScript) and pydantic models (Python) for each webhook.
- A
WebhookEventdiscriminated union that customers can use to narrow incoming requests. - README documentation listing each webhook event type.
What's not generated yet (on the roadmap):
- A
verify_signature(body, header, secret)helper. Today the customer writes their own usingcrypto.createHmacorhmac.new. - A typed handler shape (
onMessageReceived: (payload: InboundMessage) => Promise<void>). Today the customer writes a switch onevent.type.
What to do this week
- If your spec is 3.0 and you have webhooks, plan a 3.1 upgrade (the post). The
webhooks:block is the biggest single quality-of-life win in 3.1 for any team that ships event-driven APIs. - Audit your existing webhook docs against the six questions above. Most teams have 2–3 missing.
- If you ship Bloom-generated SDKs, the typed event payloads come for free once you move the webhooks to
webhooks:.