Payload schemas, signatures and retry behavior
For years webhooks were described outside the actual API documentation, in separate Markdown files that nobody kept up to date. OpenAPI 3.1 brings the new webhooks object, which anchors webhook payload schemas, security requirements and retry behavior directly in the specification.
Table of Contents
- 1. The problem: webhooks living outside the API documentation
- 2. OpenAPI 3.1: what changed
- 3. The webhooks object: structure and syntax
- 4. Payload schemas with discriminator
- 5. Documenting HMAC signature and security
- 6. Retry behavior and idempotency
- 7. Tool support: Redoc, Swagger UI, Stoplight
- 8. OpenAPI 3.0 vs. 3.1 for webhooks compared
- 9. Summary
- 10. FAQ
1. The problem: webhooks living outside the API documentation
Before OpenAPI 3.1, documenting webhooks was a solved problem without a standard. The typical solution was a separate Markdown document with JSON examples that had to be kept manually in sync with the actual webhook payload. In practice the result was that payload schemas drifted away from reality, developers relied on the real request instead of the documentation, and every change to a webhook required updates in two separate places.
OpenAPI 3.0 had an unofficial practice: documenting webhooks as callbacks, or as their own path entries with a prefix like /webhooks/order-created. That is technically inaccurate, because webhooks are not API endpoints on the server, they are HTTP requests the server sends to the client. OpenAPI 3.1 solves this cleanly with the webhooks object at the same level as paths, a dedicated section for outbound server-to-client communication.
2. OpenAPI 3.1: what changed
OpenAPI 3.1.0 was released in February 2021 and brings several relevant additions besides the webhooks object. The specification is now fully JSON Schema compatible (Draft 2020-12), which means all JSON Schema keywords can be used directly without compatibility workarounds. That matters especially for webhook payload schemas, where if/then/else conditions, unevaluatedProperties and $dynamicRef become usable.
Other 3.1 additions that affect webhook documentation: pathItems can now be defined in components and reused via $ref in both webhooks and paths. That allows defining shared webhook structures once, even when several event types share the same base payload. And license.identifier as an SPDX identifier is now officially supported, not a major feature, but useful for public API specifications.
# openapi.yaml - top-level structure with webhooks in OpenAPI 3.1
openapi: 3.1.0
info:
title: Mironsoft Order API
version: 2.0.0
description: |
REST API for order management.
## Webhooks
This API sends webhook events to configured endpoints.
All events are documented under `webhooks`.
Signature verification: see the `X-Mironsoft-Signature` header.
servers:
- url: https://api.mironsoft.de/v2
# Webhook events at the same level as paths
webhooks:
order.created:
post:
summary: Order Created Event
description: |
Fired when a new order has been created.
The receiver must return HTTP 200.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OrderCreatedEvent'
responses:
'200':
description: Webhook received successfully
'4XX':
description: Error on receipt, will be resent per the retry policy
paths:
/webhooks/subscriptions:
post:
summary: Register a webhook subscription
# ...
3. The webhooks object: structure and syntax
The webhooks object in OpenAPI 3.1 is a map object: keys are freely chosen webhook names (recommended: event names like order.created, payment.failed), values are path item objects, the same structure used under paths. That means the entire known vocabulary of path items is available: HTTP methods, requestBody, responses, parameters and security.
The HTTP method for webhooks is almost always post. The requestBody describes the payload the server sends to the consumer. The responses describe what the server expects from the consumer, typically 200 OK as confirmation, optionally 202 Accepted if processing happens asynchronously. This reversal of perspective is the conceptual difference from normal paths: in the webhook context the server is the requester and the consumer is the responder.
4. Payload schemas with discriminator
When an API sends several webhook event types that share a common base payload, the OpenAPI discriminator is the right tool. A shared field like event_type or type distinguishes the event variants. Code generators can use it to generate type-safe union types or sealed classes. Without a discriminator, the consumer would have to parse the payload manually and evaluate the type field without schema support.
The JSON Schema compatibility in OpenAPI 3.1 also allows oneOf with if/then for more complex payload structures where the discriminator approach falls short. For most use cases, oneOf with a discriminator is the clearest and most tool-friendly variant.
# components/schemas in openapi.yaml
components:
schemas:
# Base event schema - shared by all webhook events
WebhookEvent:
type: object
required: [event_type, event_id, occurred_at]
properties:
event_type:
type: string
description: Discriminator field - identifies the event variant
example: "order.created"
event_id:
type: string
format: uuid
description: Unique event ID for idempotency checks
occurred_at:
type: string
format: date-time
api_version:
type: string
example: "2.0.0"
OrderCreatedEvent:
allOf:
- $ref: '#/components/schemas/WebhookEvent'
- type: object
required: [order]
properties:
order:
$ref: '#/components/schemas/OrderPayload'
OrderCancelledEvent:
allOf:
- $ref: '#/components/schemas/WebhookEvent'
- type: object
required: [order, cancellation_reason]
properties:
order:
$ref: '#/components/schemas/OrderPayload'
cancellation_reason:
type: string
enum: [customer_request, inventory, payment_failed, fraud]
# Discriminated union for all webhook event types
AnyWebhookEvent:
oneOf:
- $ref: '#/components/schemas/OrderCreatedEvent'
- $ref: '#/components/schemas/OrderCancelledEvent'
discriminator:
propertyName: event_type
mapping:
"order.created": '#/components/schemas/OrderCreatedEvent'
"order.cancelled": '#/components/schemas/OrderCancelledEvent'
5. Documenting HMAC signature and security
Webhook security is often underrepresented in API documentation. The most common approach is an HMAC-SHA256 signature in the header: the server signs the request body with a shared secret and sends the signature as a header (X-Mironsoft-Signature: sha256=abc123...). The consumer verifies the signature before processing the payload. Without this verification, any HTTP client could forge a webhook.
In OpenAPI 3.1, the HMAC pattern can be documented as its own security scheme. Since OAuth2 and API keys are not a good fit for webhooks, a custom header security scheme is used instead. In addition, the documentation should describe how the signature is calculated: algorithm, header format and a code example in at least one common language. Many consumer developers copy this example directly into their implementation.
# Security scheme for HMAC webhook signatures
components:
securitySchemes:
webhookSignature:
type: apiKey
in: header
name: X-Mironsoft-Signature
description: |
HMAC-SHA256 signature of the request body.
**Format:** `sha256=<hex-digest>`
**Verification (PHP):**
```php
$signature = hash_hmac('sha256', $rawBody, $webhookSecret);
$expected = 'sha256=' . $signature;
if (!hash_equals($expected, $_SERVER['HTTP_X_MIRONSOFT_SIGNATURE'])) {
http_response_code(401);
exit;
}
```
**Verification (Python):**
```python
import hmac, hashlib
expected = 'sha256=' + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
assert hmac.compare_digest(expected, request.headers['X-Mironsoft-Signature'])
```
# Additional webhook-specific headers
headers:
X-Mironsoft-Event-Id:
description: Unique event ID - use for idempotency
schema:
type: string
format: uuid
X-Mironsoft-Retry-Attempt:
description: Retry attempt number (0 = first delivery)
schema:
type: integer
minimum: 0
X-Mironsoft-Delivery-Timestamp:
description: Unix timestamp of the delivery attempt
schema:
type: integer
# Webhook with a security requirement
webhooks:
order.created:
post:
security:
- webhookSignature: []
# ...
6. Retry behavior and idempotency
Webhook retry behavior is one of the most commonly undocumented aspects of webhook systems. If the consumer endpoint is unreachable or returns a non-2xx status, the server should resend the webhook. Without clear documentation of retry behavior, consumer developers do not build in idempotency logic, and on a retry the order gets processed twice.
In the OpenAPI specification, retry behavior can be documented in the webhook description and in a dedicated x-webhook-delivery extension object. This is not a standard keyword, but tools like Redoc display x- extensions in the documentation when configured accordingly. The most important thing is that the documentation answers three questions: when is a retry triggered, how many retries are there, and how should the consumer ensure idempotency.
7. Tool support: Redoc, Swagger UI, Stoplight
Redoc supports webhooks natively from version 2.1.0 onward and displays them as their own section in the sidebar. Swagger UI supports webhooks from version 5.0.0 (with limited rendering). Stoplight Elements fully supports OpenAPI 3.1 webhooks. API clients like Postman can import webhook schemas from OpenAPI 3.1 to write consumer tests.
Code generators like openapi-generator-cli support webhook schemas for most target languages since version 6.x. The generated classes can be used directly as the payload type in consumer code, including discriminator mapping for event unions. That significantly reduces manual parsing code.
| Tool | OAS 3.1 webhooks | Discriminator | Code gen | Since version |
|---|---|---|---|---|
| Redoc | Full | Yes | No | 2.1.0 |
| Swagger UI | Partial | Partial | No | 5.0.0 |
| Stoplight Elements | Full | Yes | No | 8.0.0 |
| openapi-generator-cli | Yes | Yes | Yes | 6.0.0 |
| Postman | Import | Partial | No | 10.x |
Mironsoft
OpenAPI 3.1, webhook systems and API documentation
Setting up OpenAPI 3.1 webhook documentation?
We migrate your API specification to OpenAPI 3.1, document webhooks with complete payload schemas and HMAC security documentation, and integrate everything into Redoc or Stoplight Elements.
OAS 3.1 migration
Migrate an existing openapi.yaml from 3.0 to 3.1 with JSON Schema compatibility
Webhook schemas
Payload schemas with discriminator, HMAC documentation and retry description
Tool integration
Set up Redoc, Stoplight Elements or Swagger UI and integrate into the CI pipeline
8. OpenAPI 3.0 vs. 3.1 for webhooks compared
The difference between OpenAPI 3.0 and 3.1 is particularly relevant for webhook documentation. In 3.0, the best available option was to document webhooks as callbacks of a subscription endpoint or to pack them under their own paths, both conceptually inaccurate. OpenAPI 3.1 solves this with the dedicated webhooks object and makes webhooks a first-class citizen of the specification.
9. Summary
With the webhooks object, OpenAPI 3.1 brings the long-missing standard solution for webhook documentation. Payload schemas with oneOf and discriminator enable type-safe consumer implementations and machine-readable event catalogs. HMAC signature documentation as a security scheme, retry behavior in the event description, and tool support in Redoc and Stoplight make webhooks a full-fledged part of the API specification.
The most important step: give webhook events unique names (order.created, payment.failed) and complete payload schemas. An event_id field in every payload enables idempotency checks on the consumer side. And always describe retry behavior in the documentation, so consumer developers build in idempotency logic from the start.
Webhooks in OpenAPI 3.1: the key points at a glance
webhooks object
At the same level as paths. Keys: event names. Values: path item objects with requestBody, responses and security.
Payload schemas
oneOf + discriminator for event unions. JSON Schema compatibility in OAS 3.1 enables if/then/else for complex payloads.
HMAC signature
Document as an apiKey security scheme. Code examples for PHP and Python in the description. hash_equals() for timing-safe verification.
Idempotency
event_id as a UUID in every payload. Consumer stores processed IDs. Always document retry behavior and retry intervals.