---
title: "Webhooks and Events for Agents"
description: "Designing webhook schemas and event payloads that agents can route, deduplicate, and act on reliably"
url: "https://agentsurface.dev/docs/api-surface/webhooks-events"
lastVerified: 2026-09-25
lastModified: 2026-09-25T05:46:01.000Z
---



## Summary [#summary]

Webhooks push state changes to agents, but poorly designed events cause duplicate processing and unroutable payloads. Well-designed events are self-describing, idempotent, and verifiable. OpenAPI 3.1's webhooks field lets you document incoming payloads in the same spec as your API. Each event needs a unique event\_id and idempotency\_key for deduplication, explicit event\_type for routing, and a timestamp for ordering.

* Event payloads are self-describing with event\_type and event\_id
* Include idempotency\_key to prevent duplicate processing
* Timestamp every event for reliable ordering and replay
* Document all webhook types in OpenAPI webhooks field, not separately
* Provide schema for every event payload

Agents are not always the initiating party. Many workflows begin when something happens: an order ships, a payment succeeds, a user completes onboarding. Webhooks and event streams are the mechanism for pushing these state changes to agents. Poorly designed events produce unroutable payloads, duplicate processing, and security vulnerabilities. Well-designed events are self-describing, idempotent, and verifiable.

## OpenAPI 3.1 Webhooks Field [#openapi-31-webhooks-field]

OpenAPI 3.1 introduced a top-level `webhooks` field that lets you document incoming webhook payloads in the same spec as your outbound API. This is the correct place to document webhooks; use `callbacks` under the triggering operation only for operation-specific completion events (see Webhooks vs Callbacks below).

```yaml
openapi: "3.1.0"
info:
  title: Orders API
  version: "1.0.0"

paths:
  # ... your regular API endpoints

webhooks:
  order.created:
    post:
      operationId: webhook_order_created
      summary: An order has been created
      description: >
        Sent immediately when a new order is submitted. The order will be in
        "pending" status. Listen for order.status_changed to track subsequent
        status transitions.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OrderCreatedEvent"
            example:
              event_type: "order.created"
              event_id: "evt_01HV3K8MNPQR"
              created_at: "2024-03-15T14:22:00Z"
              idempotency_key: "ord_01HV3K8M-created-1710511320"
              data:
                id: "ord_01HV3K8M"
                status: "pending"
                customer_id: "cust_xyz"
                total: 149.99
      responses:
        "200":
          description: Event acknowledged
        "202":
          description: Event accepted for async processing

  order.status_changed:
    post:
      operationId: webhook_order_status_changed
      summary: An order's status has changed
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OrderStatusChangedEvent"

  payment.succeeded:
    post:
      operationId: webhook_payment_succeeded
      summary: A payment was processed successfully
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PaymentSucceededEvent"

  payment.failed:
    post:
      operationId: webhook_payment_failed
      summary: A payment attempt failed
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PaymentFailedEvent"
```

## Webhooks vs Callbacks [#webhooks-vs-callbacks]

OpenAPI 3.x supports both `webhooks` and `callbacks`. They serve different purposes:

**`webhooks`** - events that your server sends to a registered URL, initiated by server-side state changes. The consumer registers a URL in advance; the server calls it whenever the event occurs.

**`callbacks`** - responses to long-running or asynchronous operations. Defined under a specific path operation; the server calls the consumer's URL when a previously initiated operation completes.

```yaml
paths:
  /exports:
    post:
      operationId: create_export
      summary: Start an async data export
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                format:
                  type: string
                  enum: [csv, json, parquet]
                callback_url:
                  type: string
                  format: uri
                  description: URL to receive the export.completed event
      callbacks:
        exportCompleted:
          "{$requestBody#/callback_url}":
            post:
              summary: Export completed notification
              requestBody:
                content:
                  application/json:
                    schema:
                      $ref: "#/components/schemas/ExportCompletedEvent"
```

Use `webhooks` for system-wide events (order created, payment succeeded). Use `callbacks` for operation-specific async completion events.

## Agent-Friendly Webhook Design [#agent-friendly-webhook-design]

### Discriminator Fields [#discriminator-fields]

Every event payload must include a field that unambiguously identifies the event type. Agents route events by this field - without it, the agent must infer event type from payload shape, which is unreliable.

```yaml
components:
  schemas:
    BaseEvent:
      type: object
      required: [event_type, event_id, created_at]
      properties:
        event_type:
          type: string
          description: Identifies the event type; used to route to the correct handler
        event_id:
          type: string
          format: uuid
          description: Unique identifier for this event instance
        created_at:
          type: string
          format: date-time
          description: When the event was generated (not when it was delivered)

    OrderCreatedEvent:
      allOf:
        - $ref: "#/components/schemas/BaseEvent"
        - type: object
          required: [data]
          properties:
            event_type:
              type: string
              const: "order.created"
            data:
              $ref: "#/components/schemas/Order"

    PaymentSucceededEvent:
      allOf:
        - $ref: "#/components/schemas/BaseEvent"
        - type: object
          required: [data]
          properties:
            event_type:
              type: string
              const: "payment.succeeded"
            data:
              $ref: "#/components/schemas/Payment"
```

### OpenAPI Discriminator for Polymorphic Event Handling [#openapi-discriminator-for-polymorphic-event-handling]

When multiple event types share a base schema, use the OpenAPI `discriminator` field to map `event_type` values to their corresponding schemas:

```yaml
components:
  schemas:
    AnyEvent:
      discriminator:
        propertyName: event_type
        mapping:
          "order.created": "#/components/schemas/OrderCreatedEvent"
          "order.status_changed": "#/components/schemas/OrderStatusChangedEvent"
          "order.cancelled": "#/components/schemas/OrderCancelledEvent"
          "payment.succeeded": "#/components/schemas/PaymentSucceededEvent"
          "payment.failed": "#/components/schemas/PaymentFailedEvent"
          "user.created": "#/components/schemas/UserCreatedEvent"
          "user.deleted": "#/components/schemas/UserDeletedEvent"
      oneOf:
        - $ref: "#/components/schemas/OrderCreatedEvent"
        - $ref: "#/components/schemas/OrderStatusChangedEvent"
        - $ref: "#/components/schemas/OrderCancelledEvent"
        - $ref: "#/components/schemas/PaymentSucceededEvent"
        - $ref: "#/components/schemas/PaymentFailedEvent"
        - $ref: "#/components/schemas/UserCreatedEvent"
        - $ref: "#/components/schemas/UserDeletedEvent"
```

This allows an agent framework that receives any event payload to look up the correct schema by the `event_type` discriminator value, rather than trying to pattern-match the payload shape.

### Idempotency Keys [#idempotency-keys]

Webhook delivery systems guarantee at-least-once delivery. Agents must handle duplicate events without creating duplicate side effects. Extend the `BaseEvent` schema above with an `idempotency_key`, required on every event payload:

```yaml
components:
  schemas:
    BaseEvent:
      type: object
      required: [event_type, event_id, created_at, idempotency_key]
      properties:
        event_type:
          type: string
        event_id:
          type: string
          format: uuid
          description: Unique ID for this event instance; same value on redeliveries
        idempotency_key:
          type: string
          description: >
            Stable key for deduplication. Agents should track processed
            idempotency_keys to avoid reprocessing delivered events.
            Format: {resource_id}-{event_type}-{unix_timestamp}
        created_at:
          type: string
          format: date-time
```

Document the deduplication expectation explicitly in your webhook documentation:

```yaml
webhooks:
  payment.succeeded:
    post:
      operationId: webhook_payment_succeeded
      description: >
        Sent when a payment is successfully processed. This event may be
        delivered more than once due to network retries. Use the
        idempotency_key field to detect and ignore duplicate deliveries.
        A payment is guaranteed to appear at most once per idempotency_key.
```

### Signature Verification: Standard Webhooks [#signature-verification-standard-webhooks]

Agents receiving webhooks must verify the payload signature to prevent spoofed events. Don't invent a bespoke header scheme - use [Standard Webhooks](https://www.standardwebhooks.com/), an open specification maintained by a technical steering committee including Svix, Twilio, Zapier, Kong, Supabase, ngrok, Mux, and Lob, with compatible implementations already shipped by OpenAI, Google Gemini, Brex, and others. An agent that already knows how to verify one Standard Webhooks sender can verify yours without reading your docs.

The spec defines three required headers, all prefixed `webhook-`:

```http
POST /webhooks/receive HTTP/1.1
webhook-id: msg_2KWPBgLlAfxdpx2AI54pPJ85f4W
webhook-timestamp: 1710460800
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
```

* `webhook-id` - unique identifier for this delivery
* `webhook-timestamp` - Unix timestamp (seconds) the delivery was signed
* `webhook-signature` - space-delimited list of signatures, so a secret can be rotated with zero downtime by including both the old and new signature during the overlap window

The signed content is `{webhook-id}.{webhook-timestamp}.{raw_body}`, HMAC-SHA256'd with the webhook secret, base64-encoded, and prefixed `v1,`. Signing secrets are base64, prefixed `whsec_` so tooling can recognize the scheme at a glance.

```yaml
webhooks:
  order.created:
    post:
      operationId: webhook_order_created
      description: >
        Signed per the Standard Webhooks spec (standardwebhooks.com). Verify
        webhook-signature before processing: HMAC-SHA256("{webhook-id}.{webhook-timestamp}.{body}",
        secret), base64-encoded, compared against each v1 value in the
        space-delimited webhook-signature header. Reject deliveries where
        webhook-timestamp is outside your replay-tolerance window.
      parameters:
        - name: webhook-id
          in: header
          required: true
          schema:
            type: string
          description: Unique ID for this delivery; use for support requests and dedup
        - name: webhook-timestamp
          in: header
          required: true
          schema:
            type: string
          description: >
            Unix timestamp (seconds) the delivery was signed. Reject requests
            outside your tolerance window to prevent replay attacks; the spec
            leaves the exact window to the implementer, and five minutes each
            way is a common choice.
        - name: webhook-signature
          in: header
          required: true
          schema:
            type: string
          description: >
            Space-delimited list of "v1,{base64_hmac_sha256}" values, one per
            active signing secret. Compare against all of them, not just the first.
```

## Schema Design Patterns [#schema-design-patterns]

### Exhaustive Event Type Listing [#exhaustive-event-type-listing]

List every event type your system emits. Agents building event-driven workflows need to know the full event catalog to design their listening logic.

```yaml
webhooks:
  # Order lifecycle
  order.created:
    post: { ... }
  order.status_changed:
    post: { ... }
  order.shipped:
    post: { ... }
  order.delivered:
    post: { ... }
  order.cancelled:
    post: { ... }
  order.refunded:
    post: { ... }

  # Payment lifecycle
  payment.initiated:
    post: { ... }
  payment.succeeded:
    post: { ... }
  payment.failed:
    post: { ... }
  payment.refunded:
    post: { ... }

  # User lifecycle
  user.created:
    post: { ... }
  user.email_verified:
    post: { ... }
  user.subscription_changed:
    post: { ... }
  user.deleted:
    post: { ... }
```

### Including State Snapshots [#including-state-snapshots]

Include the full resource state at the time of the event, rather than only the changed fields. Agents should not need to make a follow-up API call to get the current state after receiving an event.

```yaml
components:
  schemas:
    OrderStatusChangedEvent:
      allOf:
        - $ref: "#/components/schemas/BaseEvent"
        - type: object
          properties:
            event_type:
              type: string
              const: "order.status_changed"
            data:
              type: object
              required: [order, previous_status, new_status]
              properties:
                # Full state snapshot - agent does not need to call get_order
                order:
                  $ref: "#/components/schemas/Order"
                previous_status:
                  type: string
                  enum: [pending, processing, shipped, delivered, cancelled]
                  description: Status before this transition
                new_status:
                  type: string
                  enum: [pending, processing, shipped, delivered, cancelled]
                  description: Status after this transition
                changed_at:
                  type: string
                  format: date-time
                  description: When the status transition occurred
```

### Versioned Event Schemas [#versioned-event-schemas]

As your event schemas evolve, version them to prevent breaking changes:

```yaml
components:
  schemas:
    BaseEvent:
      type: object
      required: [event_type, event_id, schema_version, created_at]
      properties:
        schema_version:
          type: string
          enum: ["1.0", "1.1", "2.0"]
          description: >
            Version of the event schema. Consumers should check this field
            before processing. Breaking schema changes increment the major version.
            Additive changes increment the minor version.
```

## Webhook Registration Endpoint [#webhook-registration-endpoint]

Document the webhook subscription endpoint alongside the webhook definitions:

```yaml
paths:
  /webhooks:
    post:
      operationId: create_webhook
      summary: Register a webhook endpoint
      description: >
        Registers a URL to receive webhook events. The endpoint must accept
        POST requests and return 200 within 10 seconds. Use the event_types
        array to filter which events are delivered to this endpoint.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, event_types]
              properties:
                url:
                  type: string
                  format: uri
                  description: HTTPS URL to receive webhook deliveries
                event_types:
                  type: array
                  items:
                    type: string
                    enum:
                      - "order.created"
                      - "order.status_changed"
                      - "order.shipped"
                      - "order.delivered"
                      - "order.cancelled"
                      - "payment.succeeded"
                      - "payment.failed"
                  description: Events to deliver to this endpoint
                secret:
                  type: string
                  description: >
                    Optional Standard Webhooks signing secret (whsec_ prefix,
                    base64-encoded). If provided, all deliveries include a
                    webhook-signature header computed against it.
```

## Checklist [#checklist]

* [ ] All webhooks are documented in the OpenAPI `webhooks` field (not in a separate file)
* [ ] Every event payload includes `event_type`, `event_id`, and `created_at`
* [ ] Every event payload includes an `idempotency_key`
* [ ] A `discriminator` is configured on the union event schema
* [ ] Signatures follow Standard Webhooks (`webhook-id`, `webhook-timestamp`, `webhook-signature`), not a bespoke header scheme
* [ ] Signature verification rejects deliveries outside the replay-tolerance window
* [ ] State snapshots are included in event payloads (agents should not need follow-up calls)
* [ ] Every emitted event type is listed in the spec
* [ ] Event schemas are versioned with a `schema_version` field
* [ ] Webhook registration endpoint is documented in `paths`

## Related Pages [#related-pages]

* [OpenAPI for Agents](./openapi-for-agents) - spec structure that webhooks sit alongside
* [Error Handling](/docs/error-handling) - how to return errors from webhook receivers
* [Arazzo Workflows](./arazzo-workflows) - multi-step workflows triggered by webhook events
* [Orders and After Purchase](/docs/agentic-commerce/orders-and-after-purchase) - applying this design to order and payment events specifically
* [Standard Webhooks specification](https://www.standardwebhooks.com/) - header names, HMAC scheme, and rotation model this page follows
