← Back to blog

Webhook Payload Design: Fields, Size, Versioning, Security

November 1, 2026
Webhook Payload Design: Fields, Size, Versioning, Security

A good webhook payload tells the receiver what happened, when it happened, and which object it happened to. It carries just enough data to act or to fetch the rest. It is also signed, versioned, and small. Everything else is a trade-off you make on purpose.

This guide covers those decisions in order: thin vs. full, the fields, the schema, versioning, signing, and delivery. An example payload follows, built the way we would design one for change monitoring.

Gyrence
Turn Web Changes Into Structured Data
Gyrence provides web data APIs to search, fetch, extract, and map sites, returning typed responses agents can reason about.
Explore the API

TL;DR

  • Send an event type, an ISO 8601 timestamp, a unique event ID, and a data object. Keep the body usually under 20 KB.
  • Default to thin payloads (IDs plus a few state fields) when the data is sensitive or changes fast. Go fuller when receivers must work offline.
  • Put signatures, delivery IDs, and timestamps in headers. Sign the raw body with HMAC-SHA256.
  • Version through the event type, and keep old versions alive for at least six months after you announce deprecation.
  • Treat every delivery as at-least-once and possibly out of order. Consumers dedupe on event ID and re-fetch when state matters.

Table of Contents

What a webhook payload has to communicate

A webhook payload is the HTTP body a provider sends to a receiver when something happens. The Standard Webhooks spec frames it as three things: what happened, when, and the data needed to act or look further.

Test your design against one question: can a receiver that has never seen this event before route it, deduplicate it, and decide whether to fetch more, using only the body and headers? If any step needs a guess, a field is missing.

Thin or full: how much data to send

This is the first decision, and it shapes everything after it.

Thin payloadFull payload
ContentsEvent type, IDs, a few state fieldsThe whole resource
Receiver needs an API call?Usually yesUsually no
Data freshnessAlways current at fetch timeSnapshot at emit time
Sensitive data in logsMinimalWhatever the resource holds
SizeSmall, stableGrows with the resource

The Standard Webhooks spec notes that thin payloads carry identifiers and can help performance, flexibility, and data control, while full payloads save the receiver an extra call. Two practical points decide most cases:

  1. Sensitivity. Every field you add is one more thing that lands in the receiver's request logs, queues, and dead-letter storage. If a resource holds personal or financial data, a thin payload keeps it out of those places.
  2. Staleness. A full payload is a snapshot. If events can arrive late or out of order, the snapshot may already be wrong. A thin payload forces the receiver to read current state.

If you are also weighing whether to push at all, the trade-offs between webhooks versus WebSockets matter here, including how to ingest, queue, and fan out events once they arrive. For polling as a backstop, see our note on using webhooks for speed and reconciling with 15 to 60 minute polls.

A middle path works well: thin by default, plus the two or three fields most receivers branch on (status, a content hash, a changed flag). Receivers can often decide "ignore" without a round trip.

The fields every payload needs

The Standard Webhooks spec says a well-formed payload includes an event type, an ISO 8601 timestamp, and the event data, either in a data object or at the top level. We would add one more: a unique event ID in the body, so a receiver can log and dedupe without parsing headers.

  • id: unique per event, stable across retries. Not per delivery attempt.
  • type: a namespaced, versioned string such as page.changed.v1.
  • timestamp: when the event occurred, in ISO 8601 with a UTC offset.
  • data: the event-specific object. Always an object, never a bare array, so you can add keys later.

Keep infrastructure metadata (signature, delivery attempt number, delivery ID) in headers and business data in the body. Mixing them means a new piece of delivery metadata changes the shape consumers parse.

Send Content-Type: application/json. Use consistent key casing, ISO 8601 for every date, and strings for IDs that could exceed JavaScript's safe integer range.

An example payload

This is an illustrative design for a content-change event, not a documented Gyrence schema:

{
  "id": "evt_01HZX3K9",
  "type": "page.changed.v1",
  "timestamp": "2026-10-30T14:02:11Z",
  "data": {
    "monitor_id": "mon_8f2a",
    "url": "https://example.com/pricing",
    "change": "content_modified",
    "previous_hash": "sha256:9c1e...",
    "current_hash": "sha256:41b7...",
    "detail_url": "https://api.example.com/v1/monitors/mon_8f2a/changes/chg_77"
  }
}

It is about 400 bytes. The receiver knows what changed, on which monitor, and where to fetch the diff. The two hashes let it skip work when nothing meaningful moved. If you are specifying what consumers of change alerts typically need, we wrote up three requirements for content change webhooks.

Document the schema

The spec recommends publishing examples and a formal definition, such as JSON Schema or OpenAPI. Do both. Receivers generate types from a schema and copy-paste from an example, and neither replaces the other.

The same discipline applies to API responses in general. Our guide to API response schemas covers typed, discriminated shapes, and they carry over to event bodies directly.

Versioning without breaking receivers

Make versions explicit in the event type (page.changed.v1, then page.changed.v2). Additive changes, such as a new optional key, do not need a new version if receivers are told to ignore unknown fields. Renames, removals, and type changes do.

Webhook versioning and deprecation timeline

When you deprecate, announce it and keep delivering the old version. The Hook0 best-practices guide suggests supporting old versions for at least six months after the announcement. During that window, let receivers subscribe per version so they can migrate on their own schedule.

Sign it and keep it replay-safe

An unsigned endpoint accepts anything anyone posts to it. The OWASP Webhook Security Cheat Sheet recommends HMAC-SHA256 signatures, computed over the raw body bytes and sent in a header, with the timestamp included in the signed content to block replays.

What that means in practice:

  1. Providers sign timestamp + "." + raw_body and send the signature and timestamp as headers.
  2. Consumers verify against the raw bytes before parsing JSON. Re-serialized JSON will not match.
  3. Consumers reject requests whose timestamp is older than a threshold you document, and compare signatures in constant time.

Duplicates, ordering, and retries

Delivery is at-least-once. The same event will arrive twice sometimes. OWASP's guidance is that consumers should persist processed event IDs and skip repeats. That only works if your id is stable across retries, which is why it belongs in the payload design.

Ordering is not guaranteed either. Convoy's guidance treats eventual consistency as inherent to webhooks: if an invoice event arrives before the customer it depends on, the receiver should fail and let a retry resolve it. When an event might be stale, fetch the object's current state.

Response codes are part of the contract:

The same backoff logic applies on the scraping side. See exponential backoff with jitter modes and cost caps and our retries playbook. Safe repeats depend on idempotent operations too.

Our view: payloads should name their own failure modes

We build a web data API where every response, including failures, is a typed, discriminated union. We think webhook payloads deserve the same honesty.

A change alert that only says "something changed" forces the receiver to guess. A better event says what kind of change it saw, and says plainly when the fetch behind it failed, was blocked, or came back partial. Put that in a type or status field, not in a free-text message.

That is also why we lean thin. An event is a signal with a stable shape. The detail lives behind an ID, fetched at the moment you need it, under your own spending controls. Your bill and your logs both stay predictable.

Design the failure events first. If you can describe them in the schema, the happy path is easy.

Glen

Monitor changes with signed webhook alerts

If you would rather consume well-formed change events than build the monitor and the delivery pipeline yourself, WebDoppler is our monitoring product with webhook alerts. It sits on the same API primitives (Search, Traverse, Fetch, Extract, Map) as the rest of Gyrence.

Gyrence

Plans are published on the pricing page: Standard is $75 per month, Growth is $299 per month, and Scale is $549 per month. There is also a Free plan and Pay-As-You-Go metering, and spending caps apply either way.

A checklist before you ship

  • Does every event have a stable id, a type with a version, and an ISO 8601 timestamp?
  • Is data an object, so new keys are additive?
  • Is the body well under 20 KB, with no fields you would regret seeing in a log?
  • Are signature, timestamp, and delivery ID in headers, and is the timestamp inside the signed content?
  • Is there a JSON Schema or OpenAPI definition plus a copy-pasteable example?
  • Are retry schedule, 410 and 429 behavior, and the deprecation window documented?
  • Is there a reconciliation path, such as a list endpoint or a periodic poll, for events a receiver misses?

FAQ

Should a webhook payload include the full resource?

Only if receivers benefit from a snapshot and the data is not sensitive. Otherwise send identifiers and a few state fields, and let the receiver fetch current data. Thin payloads also keep sensitive fields out of the receiver's logs.

How big can a webhook payload be?

The Standard Webhooks spec recommends keeping payloads small, usually under 20 KB. If a resource regularly exceeds that, send a reference and let the receiver fetch the rest.

Where should the signature go, the header or the body?

In a header. Sign the raw body bytes with HMAC-SHA256 and include the timestamp in the signed content. The receiver then verifies before parsing anything.

How do I stop duplicate events from being processed twice?

Give every event a unique ID that stays the same across retries. The consumer stores processed IDs and skips any it has seen, as OWASP recommends. Pair that with a re-fetch when the order of events matters.

Sources