Webhook Versioning Strategies for Outbound Webhook Providers
Your webhook payloads will change. New fields get added as your product evolves, existing fields get renamed as your data model matures, and entire structures get reorganized when you realize the original design doesn't scale. This is normal and inevitable.
What isn't inevitable is breaking every consumer integration in the process. That outcome is a choice, and it's one you make (or avoid) by having a versioning strategy before your first consumer goes live.
Versioning sounds straightforward in the abstract: you change the payload, you bump a version number, consumers upgrade when they're ready. In practice, it's one of the more nuanced decisions in webhook infrastructure design, because the constraints on webhook versioning are different from REST API versioning. With an API, consumers initiate requests and can control which version they call. With webhooks, you push data to consumers, and they have to handle whatever shows up at their endpoint.
This post covers the major approaches to webhook versioning, the trade-offs each involves from both the provider and consumer perspective, and practical guidance on managing the transition when breaking changes are unavoidable.
What counts as a breaking change
Before choosing a versioning strategy, you need to define what "breaking" means for your consumers. Not every payload modification requires a version bump.
Generally safe (non-breaking):
- Adding new fields to an existing payload
- Adding new event types
- Changing the order of fields in a JSON object
- Extending an enum with new values (if consumers handle unknown values gracefully)
- Increasing the length of string fields
Generally breaking:
- Removing a field
- Renaming a field
- Changing a field's type (integer to string, object to array)
- Changing the nesting structure of the payload
- Altering the semantics of an existing field (same name, different meaning)
- Changing the envelope structure (moving metadata fields, changing how event types are represented)
The "generally" qualifier matters. Whether adding a new enum value breaks consumers depends on whether they wrote strict switch statements or tolerant parsers. Your versioning strategy needs to account for the least defensive consumer code in production.
Document your compatibility guarantees explicitly. Consumers need to know which changes you consider non-breaking so they can write their code accordingly.
The four main versioning strategies
Global API versioning (date-pinned)
The consumer subscribes at a specific API version, and all webhooks they receive use that version's payload schema until they explicitly upgrade.
Stripe is the canonical example. Each account is pinned to an API version identified by a date (e.g., 2026-01-28). Webhook endpoints inherit the account's API version at the time the event is created. When Stripe ships a breaking change, existing consumers continue receiving the old payload format. They upgrade on their own schedule by changing their account's API version, then updating their webhook handler to match.
{
"id": "evt_1abc2def",
"api_version": "2026-01-28",
"type": "invoice.paid",
"data": {
"object": {
"id": "inv_4k9x7m",
"amount_paid": 4999
}
}
}
How it works in practice: You maintain a set of version transforms internally. When generating a webhook payload, you build the current version and then apply backward-compatible transformations to produce the schema the consumer expects. A consumer pinned to a version from six months ago gets a payload that matches what they built against, even though your internal data model has evolved significantly.
Provider pros:
- Clear contract with consumers: each version is a snapshot of behavior
- Consumers upgrade on their own timeline, reducing support load
- Breaking changes can ship without coordinating with every consumer simultaneously
Provider cons:
- You must maintain payload generation for every supported version simultaneously
- The version transform layer grows in complexity over time
- Testing multiplies: every new feature needs validation against all active versions
- You need a deprecation policy and enforcement mechanism to retire old versions
Consumer pros:
- Full control over when to adopt changes
- No surprise breakages from provider-side updates
- Can test against a new version before committing to it
Consumer cons:
- Must eventually upgrade or risk being on an unsupported version
- Upgrade requires reading changelogs and modifying handler code
- If they fall many versions behind, the cumulative migration can be daunting
Best suited for: Large platforms with many consumers, long-lived integrations, and the engineering resources to maintain a version transform layer. If your consumers are enterprises with slow change cycles, this approach respects their pace.
Additive-only evolution (no explicit versions)
You commit to never removing or renaming fields. Payloads only grow: new fields appear, but existing ones stay put. Consumers ignore fields they don't recognize.
This is the approach many smaller webhook providers take, either by design or by default. There's no version number anywhere. The payload just gradually accumulates new fields over time.
{
"type": "order.shipped",
"data": {
"order_id": "ord_123",
"tracking_number": "1Z999AA10123456784",
"carrier": "ups",
"estimated_delivery": "2026-03-01",
"carbon_offset_applied": true
}
}
The carbon_offset_applied field was added six months after launch. Consumers who don't care about it never notice. Consumers who do can start using it immediately.
Provider pros:
- No version management overhead whatsoever
- No transform layer to build or maintain
- Every consumer gets the same payload, simplifying debugging and support
- New features are instantly available to all consumers
Provider cons:
- You can never remove a field, even if it's been deprecated for years
- You can never rename a field, even if the original name was a mistake
- Payload size grows monotonically as you accumulate dead weight
- The moment you need a genuinely breaking change, you have no mechanism for it
Consumer pros:
- No version pinning or upgrade ceremonies
- New fields appear automatically
- Existing code never breaks (in theory)
Consumer cons:
- Must write tolerant parsers that ignore unknown fields
- No guarantee that field semantics won't subtly shift
- If the provider eventually does ship a breaking change, there's no versioning infrastructure to soften the impact
Best suited for: Webhook systems with simple payloads, a small number of event types, and providers confident they won't need structural changes. Works well in early stages but can become a constraint as the product matures.
Per-event-type versioning
Each event type carries its own version number, independent of other event types. An order.created payload might be at v3 while customer.updated is still at v1. Consumers subscribe to specific versions of specific event types.
{
"type": "order.created",
"version": "3",
"data": {
"id": "ord_456",
"line_items": [...]
}
}
How it works: The version is typically included in the payload (as a top-level field) or encoded in the event type itself (e.g., order.created.v3). When you ship a breaking change to one event type, only consumers of that specific event are affected.
Provider pros:
- Breaking changes are scoped: updating the order schema doesn't touch customer events
- Consumers who only use a few event types have a smaller upgrade surface
- Versions advance independently, so infrequently-changed events don't accumulate pointless version bumps
Provider cons:
- Tracking which consumers are on which version of which event type is complex
- Documentation must cover version matrices across event types
- Testing requires covering many version combinations
- Internal consistency across event types can drift (e.g., order v3 references customer objects, but which version of the customer schema?)
Consumer pros:
- Only need to upgrade the specific event types they use
- Can adopt new versions of high-priority events while deferring others
- Version changes are clearly scoped and documented per event type
Consumer cons:
- Must track version numbers per event type, not just globally
- Cross-event-type consistency isn't guaranteed
- Subscribing to the right combination of versions requires careful management
Best suited for: Providers with diverse event types that evolve at different rates, particularly when some events are stable while others undergo frequent structural changes.
Topic-based versioning
The version is encoded in the topic (event type) itself, and consumers subscribe to the versioned topic they support. Rather than a metadata field, the version becomes part of the routing key.
order.created.v2
customer.updated.v1
invoice.paid.v3
When you introduce a breaking change to order.created, you publish to both order.created.v2 (the old schema) and order.created.v3 (the new one) during a transition period. Consumers subscribed to v2 keep receiving the old format. New consumers or upgrading consumers subscribe to v3.
Provider pros:
- Versioning is explicit and visible in the subscription model
- Standard topic-based routing infrastructure handles version selection
- Consumers self-select their version through their subscription, no per-consumer configuration needed
- Clean deprecation path: stop publishing to the old topic after the transition period
Provider cons:
- Topic proliferation: every event type multiplied by every active version
- Must dual-publish during transitions, increasing delivery volume
- Consumers must update their subscriptions to upgrade, not just their handler code
- Topic naming conventions must be established and enforced consistently
Consumer pros:
- Version selection is part of the subscription, making it explicit and visible
- Can subscribe to different versions of different event types
- Migration is clear: subscribe to the new topic, update handler code, unsubscribe from the old topic
Consumer cons:
- Must change subscription configuration to adopt a new version (not just handler code)
- During transitions, might receive events from both old and new topics if not careful
- More topics to manage in their subscription configuration
Best suited for: Webhook systems built on topic-based routing infrastructure where subscriptions are a first-class concept. This approach works particularly well when the delivery layer already supports topic filtering.
Comparing the strategies
| Strategy | Complexity | Breaking change handling | When to use |
|---|---|---|---|
| Global API versioning |
|
|
|
| Additive-only |
|
|
|
| Per-event-type |
|
|
|
| Topic-based |
|
|
|
Managing the transition: migration playbooks
Whichever strategy you choose, the mechanics of actually transitioning consumers from one version to another matter as much as the strategy itself.
The overlap window
Never cut over instantly. When introducing a new version, maintain an overlap period where both the old and new versions are available simultaneously. The length depends on your consumer base: a few weeks for a developer tool with technically savvy users, a few months for a platform serving enterprises with quarterly release cycles.
During the overlap, both versions are active. For global versioning, consumers are still pinned to the old version and can upgrade when ready. For topic-based versioning, you publish to both the old and new versioned topics.
Communicating changes
Your changelog is the single most important artifact in a version transition. For each breaking change, document:
- What changed (the specific field, type, or structure)
- Why it changed (context helps consumers understand the direction)
- Before and after payload examples
- Migration steps: exactly what code changes the consumer needs to make
- Timeline: when the old version will be deprecated and eventually removed
Deprecation policy
Define your policy before you need it. A reasonable baseline: announce deprecation at least 90 days before removal, send reminder notifications at 60, 30, and 7 days, and include the deprecation timeline in every relevant API response or webhook delivery during the wind-down period.
Testing support
Give consumers a way to test against the new version before committing. This might be a sandbox environment that sends the new payload format, a CLI tool that generates sample payloads for both versions, or an endpoint that lets consumers request a sample event in either version.
How Outpost supports versioning
Outpost, Hookdeck's open-source webhook infrastructure, doesn't prescribe a single versioning strategy. Instead, it provides the primitives that let you implement whichever approach fits your product.
Topic-based versioning with subscriptions
Outpost's topic system is the most natural fit for versioning. You define your available topics via the TOPICS configuration, and destinations subscribe to the topics they want to receive. By encoding the version in the topic name (e.g., order.created.v2), consumers self-select their version through their subscription.
When you introduce a breaking change, you add the new versioned topic to your configuration and begin publishing to both the old and new topics during the transition period. Consumers on the old version continue receiving events unchanged. Consumers ready to upgrade update their destination's topic subscriptions. When the deprecation window closes, you stop publishing to the old topic and remove it from the configuration.
This approach leverages the infrastructure Outpost already provides for topic filtering and destination management, without requiring any additional versioning layer.
Metadata for version headers
Outpost's metadata system converts event metadata key-value pairs into HTTP headers on delivery. By including a version identifier in your event metadata, consumers receive a version header with every delivery.
{
"topic": "order.created",
"metadata": {
"schema-version": "2026-02-01"
},
"data": {
"id": "ord_789",
"total": 4999
}
}
The consumer receives an x-outpost-schema-version: 2026-02-01 header (using the configured header prefix) alongside the standard x-outpost-topic, x-outpost-id, and x-outpost-timestamp headers. This supports additive-only or global versioning strategies where the version is communicated but routing isn't changed.
Destination filters for version routing
If you publish events with a version field in the payload, Outpost's destination filters can route events to destinations based on that field. A consumer's destination can be configured to only receive events where data.api_version matches a specific value, effectively implementing per-consumer version pinning without topic proliferation.
Version migration with the tenant portal
The tenant user portal gives consumers self-service control over their destinations, including topic subscriptions. During a version migration, consumers can update their own subscriptions to the new versioned topics, inspect events in both the old and new formats, and manually retry events if their handler needs adjustment. This reduces the support burden of version transitions by giving consumers the tools to manage the migration themselves.
Start before you need it
The most common versioning mistake isn't choosing the wrong strategy. It's not choosing one at all, then scrambling to retrofit versioning after the first breaking change has already been shipped and consumers are already filing tickets.
Even if your current payloads are simple and stable, establish your approach now. At minimum: document your compatibility guarantees (what you consider a breaking vs. non-breaking change), include a version indicator in your payloads or headers from day one (even if it stays at v1 for months), and commit to a deprecation policy in writing.
The versioning infrastructure you build before your first breaking change is an investment. The versioning infrastructure you build after your first breaking change is damage control.