Documenting Webhooks in OpenAPI 3.1
AI generated
{ }
GET
OpenAPI 3.1 · Webhooks · API Documentation · HMAC
Documenting Webhooks in OpenAPI 3.1
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.

11 min read OpenAPI 3.1 · webhooks object · HMAC-SHA256 · Redoc · Swagger UI OpenAPI Specification 3.1.0

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.

10. FAQ: Documenting webhooks in OpenAPI 3.1

1From which OpenAPI version does webhooks exist?
From OpenAPI 3.1.0 (February 2021). In 3.0.x only workarounds via callbacks or paths are possible.
2webhooks vs. paths, what is the difference?
paths = server endpoints that clients call. webhooks = HTTP requests the server sends to consumers. Reversed perspective: requestBody is the payload from the server.
3What is the discriminator for webhooks?
A payload field (e.g. event_type) that distinguishes event types. With oneOf and discriminator.mapping, code generators produce type-safe union types.
4How do I document the HMAC signature?
As an apiKey security scheme with in: header. The description contains the algorithm, format and code examples for PHP/Python. Reference it under security in the webhook entry.
5Does Redoc support OAS 3.1 webhooks?
Yes, from Redoc 2.1.0 onward, with its own sidebar section, payload schema rendering and security notes.
6How do I ensure idempotency with webhooks?
event_id (UUID) in every payload. Consumer stores processed IDs in Redis with a TTL. A retry delivers the same event_id, the consumer recognizes the duplicate and returns 200.
7OAS 3.0 vs. 3.1 for webhooks?
3.0: no webhooks object, only workarounds. 3.1: dedicated webhooks object, JSON Schema Draft 2020-12, pathItems reusable in components.
8Code generation from webhook schemas?
openapi-generator-cli from 6.0.0 onward. Discriminator mapping produces type-safe union types in Java, TypeScript, Kotlin.
9How do I document retry behavior?
In the webhook description: maximum retries, intervals (exponential backoff), triggering HTTP status codes, and the deactivation threshold.
10Must the consumer use HTTPS?
Always. Webhook payloads can contain sensitive data. The HMAC signature does not protect anything without HTTPS. State this as a requirement in the documentation.