Designing API Webhook Headers: Beyond Signature, What Every Delivery Should Carry

Webhook deliveries are HTTP requests, and HTTP requests carry headers, and most webhook providers underuse the header surface. The right set of headers makes signature verification cleaner, makes replay handling possible, and lets customers debug delivery problems without instrumenting their ow

A webhook delivery is an HTTP POST request from a provider to a customer-controlled URL. The body carries the event payload. The headers carry everything else: signature, timestamp, event metadata, delivery attempt context. Most providers carry less than they should, which produces a tax on every customer integration in the form of debugging time and edge-case bugs that would be avoidable with better instrumentation in the request itself.

The minimum useful set of headers is larger than most providers ship by default. The right set is small enough to remain readable, large enough to support the operational and debugging work customers actually do.

The signature pair

Every webhook delivery should carry a signature header and a timestamp header. The signature is computed over the raw body bytes and the timestamp, producing a value the receiver verifies by recomputing with the shared secret. The pair defends against three failure modes: tampering, replay, and misrouting.

The conventional names are X-Webhook-Signature for the HMAC value and X-Webhook-Timestamp for the timestamp. The value format is best as v1=hexsignature or similar, with the version prefix supporting future algorithm upgrades without breaking parsing. The timestamp format is best as a Unix epoch in seconds, which is unambiguous and parseable everywhere, rather than an ISO 8601 string which has timezone parsing edge cases.

The timestamp is what makes replay protection possible. The receiver checks that the timestamp is within a tolerance window (5 minutes is the conventional default) before accepting the delivery. Without the timestamp, an attacker who captures a delivery can replay it indefinitely; with the timestamp, the replay window is bounded.

The event identifier

Every delivery should carry a stable event identifier in a header like X-Webhook-Event-Id. The identifier is the same value the provider would expose in its API for the event resource, and it is the value the receiver uses for idempotency. The convention of putting it in a header rather than relying on customers to extract it from the body is the difference between idempotency that is trivially implementable and idempotency that requires JSON parsing before deduplication.

The same logic applies to X-Webhook-Event-Type, which carries the event type as a string like invoice.created. The header lets receivers route deliveries to type-specific handlers without parsing the body first, which is the common pattern in any non-trivial webhook receiver. Putting the type in a header lets the parsing step happen only after routing has selected the right handler with knowledge of the expected schema version.

The delivery context

Delivery context headers are the ones most often missing and most useful for debugging. The set includes X-Webhook-Delivery-Id (a unique identifier for this specific delivery attempt, distinct from the event ID), X-Webhook-Attempt (which attempt this is, starting at 1), and X-Webhook-Subscription-Id (which subscription configuration produced this delivery).

The delivery ID matters because a single event can produce multiple deliveries to a single subscription if the first attempts failed. The attempt counter matters because customer-side log analysis frequently needs to distinguish "we received this twice because we have a retry happening" from "we received this twice because we have a bug in our deduplication." The subscription ID matters for customers with multiple subscriptions to the same event, where the question of which subscription produced a given delivery becomes the difference between an actionable log and a confusing one.

The version pin

Every delivery should carry a header indicating the event schema version, like X-Webhook-Api-Version or X-Webhook-Schema-Version. The header lets customers pin their parsing to a specific version and produce a clear error when they receive an unexpected version, rather than silently misinterpreting a field that has changed meaning.

The pattern is more important than it sounds because webhook schemas evolve over years. A customer who integrated with v1 in 2024 and is still receiving deliveries in 2026 needs the version header to recognize that the format has not changed for their subscription, even if the provider has rolled out a v2 for newer subscriptions. Without explicit versioning, schema evolution either freezes the schema forever or produces silent breakage.

Standard HTTP headers that matter

Beyond the webhook-specific headers, three standard HTTP headers carry weight in the webhook context. Content-Type should always be set explicitly to application/json rather than left to default, because some HTTP libraries default to application/x-www-form-urlencoded when the body looks form-shaped. User-Agent should identify the provider, ideally with version information, so customers can filter their logs by source. And Idempotency-Key can be set to the event ID as a parallel signal to the custom event ID header, which lets customers using libraries that already understand idempotency keys (notably anything modeled on Stripe's API) take advantage of existing dedup machinery.

What headers should not carry

The temptation with headers is to keep adding them. Resist. Three categories of information do not belong in headers and are wrong choices when the question comes up.

Authentication tokens for the receiver do not belong in headers. The signature pair is the authentication mechanism. Adding a separate bearer token or API key in a header creates a confused threat model: if the signature is sufficient, the token adds nothing; if the token is sufficient, the signature is wasted work; and the actual semantics are usually "we want to confuse attackers" which is worse than picking either one and committing to it.

Sensitive payload fields do not belong in headers. A delivery for a payment event should not have the amount or customer name as headers, even when the receiver might find them convenient. Headers are typically logged by proxies and load balancers, which leaks sensitive data into infrastructure the customer does not control. The body is what gets logged with care; the headers should carry routing information, not content.

Operational metadata about the provider's internal systems does not belong in headers. Provider-internal request IDs, internal queue identifiers, internal customer IDs—these belong in provider-side logs accessed via the provider's dashboard, not surfaced to customers who have no use for them and no ability to interpret them.

The customer-facing contract

The headers a webhook delivery carries are a contract with the customer. The contract should be documented in the provider's webhook documentation, with example deliveries showing every header that will be present and a list of headers that are explicitly reserved for future use so customers do not collide with them.

The contract should also distinguish between stable and unstable headers. The signature, timestamp, event ID, event type, delivery ID, attempt, subscription ID, and version are the stable contract. Provider-specific debugging headers, internal trace IDs, or experimental features should be prefixed (X-Webhook-Beta- or similar) to make it clear that customers should not depend on them.

What we do

Our four products converge on similar header sets despite different domains. WebhookVault as a webhook-receiving product has the most opinions: we ship X-Webhook-Signature, X-Webhook-Timestamp, X-Webhook-Event-Id, X-Webhook-Event-Type, X-Webhook-Delivery-Id, X-Webhook-Attempt, X-Webhook-Subscription-Id, and X-Webhook-Api-Version on every delivery. CronPing for monitor-state-change webhooks ships a subset because monitor events have a smaller schema. FlagBit for flag-change webhooks ships the same subset. DocuMint emits no outbound webhooks but receives Stripe webhooks, and we honor the Stripe header convention which is structurally similar but uses Stripe-specific names.

The pattern that the four products converge on from independent design suggests that the design space for useful webhook headers is narrow. The set of headers customers actually use to integrate, debug, and operate webhook receivers is small and stable. Providers that ship less than this set produce avoidable customer support tickets; providers that ship more than this set add noise that customers ignore. The middle path is the practical one.


Our products: DocuMint (PDF invoice generation API), CronPing (cron job monitoring with status pages), FlagBit (feature flags API for modern teams), and WebhookVault (webhook capture and replay) put these patterns into production.

Read more