Skip to content

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 conditionExpected verification resultBusiness effect
Valid provider test eventAcceptedOnly the approved event handler runs
Correct body, wrong endpoint secretRejectedNo state change or job creation
Changed body, original signatureRejectedNo partial processing
Missing required signature fieldRejectedControlled error response
Oversized requestBounded rejectionNo 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 itemDecision to publish
Trigger and payloadExact event condition, field meaning, identifiers, and time units
SignatureAlgorithm, header format, signed bytes, timestamp policy, key source
AcknowledgementWhich status means durable acceptance, not completed business work
RetriesRetryable failures, backoff, maximum delivery age, and manual resend behavior
DeduplicationStable event ID, account/environment scope, and retention period
VersionEvent schema version, migration window, and subscription selection
DisablementSupported 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.

Sources

  1. Stripe: Receive events in your webhook endpointAccessed
  2. OpenAPI Specification 3.1.2: Webhooks and CallbacksAccessed
  3. OpenAPI Initiative: Webhook ExampleAccessed