Document OpenAPI Webhooks and Verify Inbound Signatures
Model OpenAPI 3.1 webhook direction, payloads, and delivery rules, then verify raw-body signatures, tenant mapping, retries, and duplicates.
Treat an inbound webhook body as an untrusted message until the provider's verification succeeds. A familiar event name or an account identifier in the JSON does not authenticate the sender. Identify the exact provider, endpoint, environment, and signing-secret source before inspecting business logic.
Stripe's webhook documentation requires the original request body for signature verification. Other providers have their own signed fields and algorithms. Use the supported verifier for the integration rather than adapting an example from an unrelated provider.
Model the sender and receiver
In OpenAPI 3.1, top-level webhooks describes provider-initiated requests that an API consumer may implement. The provider sends the POST; the customer's receiver accepts its body and returns the documented response. The map key is a webhook name, not a receiver URL. Registration supplies the actual destination.
This complete example describes one event. It contains no executable signing implementation:
openapi: 3.1.2
info:
title: Invoice Events
version: 1.0.0
webhooks:
invoicePaid:
post:
operationId: receiveInvoicePaid
summary: Sent after an invoice is marked paid.
security:
- webhookSignature: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [id, type, invoiceId]
properties:
id: {type: string}
type: {type: string, const: invoice.paid.v1}
invoiceId: {type: string}
responses:
'202':
description: Verified event durably accepted.
'400':
description: Invalid payload or signature.
'503':
description: Not accepted; follow the retry policy.
components:
securitySchemes:
webhookSignature:
type: apiKey
in: header
name: X-Webhook-Signature
description: Signature carrier, not a static API key.
The apiKey scheme documents where the signature header travels. It does not define the signing algorithm, generate a verifier, or mean that the signature is a reusable API key. Keep webhook signing material separate from API access credentials. Add the actual signed-input, timestamp, key-selection, and rotation rules to the delivery contract.
Use operation-level callbacks when the outgoing request is tied to a parent API operation and its destination can be expressed from that operation's request or response. Independent event subscriptions belong under webhooks; OpenAPI 3.0 lacks that top-level field. With SSE, the consumer opens the streaming connection instead of receiving a new webhook POST. These are different delivery models.
Generated payload types are useful, but generator support varies. Inspect the chosen version's output and test a signed fixture. A typed event model or a declared header does not implement verification.
Trace bytes before parsing
Map every middleware stage between the network and verification. Body parsers, character conversion, logging helpers, and proxy transformations can alter the material being checked. Keep the request bounded even while preserving the required raw representation.
| Input condition | Expected verification result | Business effect |
|---|---|---|
| Valid provider test event | Accepted | Only the approved event handler runs |
| Correct body, wrong endpoint secret | Rejected | No state change or job creation |
| Changed body, original signature | Rejected | No partial processing |
| Missing required signature field | Rejected | Controlled error response |
| Oversized request | Bounded rejection | No unbounded buffering |
Use the provider's test tooling or locally controlled signed fixtures. Do not obtain production event bodies merely to make the test realistic.
Keep verification separate from authorization
A valid signature proves the signed message passed the provider-specific check. It does not mean every event type should trigger every operation. After verification, check the event type, relevant environment, account mapping, and the handler's permitted action.
Select the verification secret from trusted endpoint configuration. Do not let an unverified payload choose an arbitrary secret or tenant. If the provider supports multiple accounts, document the authenticated mapping and how a failed lookup is handled.
Record failures without leaking credentials
Log a bounded reason code, endpoint identifier, and internal correlation value. Avoid logging signing secrets, signature headers, or full sensitive payloads. A failed event should not reach a general retry queue that later skips verification.
Retain test results for the actual deployed middleware stack, not just the verifier function. Check secret rotation with the provider's documented overlap procedure and remove expired verification material when the transition ends. Add replay and duplicate-effect tests separately: authenticity is necessary, but it does not make repeated delivery harmless.
Specify delivery, deduplication, and versions
Attach this contract to each event definition before consumers build handlers:
| Contract item | Decision to publish |
|---|---|
| Trigger and payload | Exact event condition, field meaning, identifiers, and time units |
| Signature | Algorithm, header format, signed bytes, timestamp policy, key source |
| Acknowledgement | Which status means durable acceptance, not completed business work |
| Retries | Retryable failures, backoff, maximum delivery age, and manual resend behavior |
| Deduplication | Stable event ID, account/environment scope, and retention period |
| Version | Event schema version, migration window, and subscription selection |
| Disablement | Supported unsubscribe operation and handling of permanent failures |
Do not promise exactly-once delivery. Stripe documents retries, duplicate events, and delivery without an ordering guarantee; other senders need their own contract. Persist accepted event identity and processing state so a repeated delivery cannot repeat the effect. Test a crash between storage and processing, plus concurrent duplicates. Set retention to cover the agreed retry and resend horizon.
For an API you own, version incompatible payload changes explicitly: use a new event type such as invoice.paid.v2 or a separately versioned subscription endpoint. Keep the old shape available during the agreed migration period. The OpenAPI document's info.version does not itself negotiate a payload version with receivers.
Verify that rendered docs expose the trigger, payload, signature requirements, responses, and delivery policy. Test old and new payload fixtures separately. Document how subscribers disable delivery; neither OpenAPI nor a generic HTTP 410 response creates a universal unsubscribe rule.
Find the gaps before your next launch
Use the release checklist to identify changed security boundaries and missing evidence. Discuss the application paths you want reviewed before launch.