Svix Blog
Published on

Webhook Versioning: How to Version Webhooks

Authors

Cover image

Svix is the enterprise ready webhooks sending service. With Svix, you can build a secure, reliable, and scalable webhook platform in minutes. Looking to send webhooks? Give it a try!

Webhooks, like the rest of your API, are an unbreakable contract. If you make backwards-incompatible changes to your API you will break your customers' integrations, and it's the same case with webhooks. However your webhooks and APIs will usually need to evolve with the product, so what do you do?

You have a variety of methods to version an API, with the most common ones being path based and passing version information in a header.

Path based, essentially means having different paths for different API endpoint versions, e.g:

# Version 1
curl https://api.svix.com/api/v1/application

# Version 2
curl https://api.svix.com/api/v2/application

While header based would look something like this:

curl https://api.svix.com/application \
  -H "Api-Version: 2024-11-07"

Each come with their own advantages and disadvantages, but the gist of it is that API consumers can choose the API version when making an API call. This gives API consumers a lot of flexibility to choose the version of API they interact with as they use it.

The problem with webhooks is that it's asynchronous and event based, which means that you don't make an explicit call for each event, but rather you're consuming events sent to you. This means that you can't define a different version on a per-event basis, you need to choose a version ahead of time when configuring the webhook endpoint.

Similarly to APIs there are multiple ways of achieving this, each with their own trade-offs. In these post we'll cover the most popular options and our recommended approach based on our experience, as well as conversations and learnings from other webhooks teams at top companies such as Stripe, Github, Zoom, and the likes.

Best versioning scheme: don't break your webhooks API

The purpose of webhook versioning is to avoid breaking your customers' integrations when breaking your webhooks API. Though a much easier way of not breaking your webhooks API is: not to break your webhooks API.

Not breaking your webhooks API is easier than it sounds. Adding a field, for example, is a non-breaking change. You can add a field to a webhook payload without it breaking an webhook API.

{
  "name": "John"
}

Adding surname to the payload is non breaking:

{
  "name": "John",
  "surname": "Doe"
}

Sometimes you may want to rename a field or change the structure a bit, while better to avoid these cosmetic changes if possible, you can easily do it in a non-breaking manner by duplicating data.

The above example can change to:

{
  "name": "John",
  "surname": "Doe",
  "fullName": "John Doe"
}

or to

{
  "name": "John",
  "surname": "Doe",
  "person": {
    "name": "John",
    "surname": "Doe"
  }
}

without being a breaking change.

While this adds a bit of redundant data to each payload, you can just mark these fields as deprecated in your docs and people will ignore them.

Similarly to a normal API, another thing you can do to avoid breaking changes, is to be thoughtful about what you add before you add it. This is where "thin payloads" come into play.

For example, consider this payload:

{
  "type": "contact.created",
  "timestamp": "2022-11-03T20:26:10.344522Z",
  "data": {
    "id": "1f81eb52-5198-4599-803e-771906343485",
    "type": "contact",
    "fullName": "John Smith",
    "address": "800 W NASA Pkwy, Webster, TX 77598, USA",
    "phoneNumber": "(281) 332-2575",
    "birthday": "1980-04-19",
    "occupation": "Engineer, ACME"
    ...
  }
}

The more fields you have and the more information you send, the more likely it is to want to make changes down the line. While this lightweight payload is much less likely to require changes:

{
  "type": "contact.created",
  "timestamp": "2022-11-03T20:26:10.344522Z",
  "data": {
    "id": "1f81eb52-5198-4599-803e-771906343485"
    "type": "contact",
    "fullName": "John Smith",
  }
}

This means that if you're thoughtful about what fields you add to the payload, instead of just including everything by default, you're much less likely to need breaking changes.

Account or endpoint level versioning

One method for webhook versioning, that's employed by Stripe, Shopify, and a few others is having a global version identifier on either the account or the endpoint. For example, this is how it looks in the Stripe dashboard:

Stripe webhook versioning UI

The way it works, is that once you set the version of the endpoint, all the webhooks going to this endpoint will follow the new version scheme.

The nice thing about this method is that it's very simple. You just set the version of the endpoint and everything is updated. Additionally it forces your webhook consumers to stay up to date, because if they want to use a new version of an event type, they'll be forced to update all of the other event types as well.

The main disadvantage with this approach is that it's "all of nothing". You can't just update one event type handler, you have to update them all. This means, that if for example you're listening to 50 event types, you'll have to update 50 code paths in tandem making the upgrade much more risky.

To make matters even worse, 48 of these event types may trigger consistently, but the last 2 may only trigger once a month. So you may end up upgrading to the newer version, use the handlers for a couple of weeks, and then start facing issues without the ability to downgrade to an older version as that will require downgrading all of them as well.

Better alternative: on a per event-type basis

Event types are the core type identifier of webhooks. A webhook provider usually supports sending a variety of events, each tagged with their own event type.

Instead of having the global webhook version as shown in the previous section, you can have each event type have an associated version. This enables your webhook consumers to choose different versions for different event types and update gradually.

Snyk, Svix, and a few others follow this pattern. For example with Snyk, you can add a version suffix to each event type like so: project_snapshot/v0. In Svix you prefix the version in front of the event type like: v2.invoice.paid.

This gives your webhook consumers the ability to update event types one at a time, thoroughly testing each upgrade, as well giving them the ability to just downgrade a specific event type if their upgrade had issues. It's also very simple to implement, it's just a different event type!

Additionally, you don't have to worry about adding version prefixes to your event types just yet. You can just start by naming your event types without a prefix, and when you're ready to add versioning info you can add the version prefix then. So start with invoice.paid and only add v2.invoice.paid once you'd like to introduce a breaking change. Or in short: you can worry about this later.

How it works in Svix

At Svix, we recommend following the per event-type method. It offers the most flexibility for you as a webhook sender, and the most robust experience as a webhook consumer. To support that Svix has multiple features that you can utilize in order to make per event-type webhook versioning a breeze.

Feature flags: Svix supports having feature flags for event types. Feature flags enable you to only show/allow a certain set of event types for a specific customer. Using feature flags you can, for example, show customer A that signed up in 2022 all the legacy event types from 2022 as well as the ones from 2024, or any other advanced setup to make sure you only allow them to use the events they should be allowed to use.

Event-type grouping in the UI: when creating event types you can set both the event type's name as wall as the groupName. The group name lets you change how the event type is grouped in the UI, letting you group and order events such as v2.invoice.paid as if they were called invoice.paid leading for nicer presentation.

Transforming schema using Connectors: the Svix Connectors functionality allows Svix customers to write payload transformations that can transform one payload structure to another using JavaScript. So you can write a transformation that transforms a newer version webhook to an old webhook schema, and let your customers choose it when integrating. This enables you to not have to worry about the versioning, while making it easy for your customers to maintain backwards compatibility.

Closing words

There are always trade-offs, but we feel like per event-type versioning is the best option available for webhook senders for the reasons outlined above. If you have any thoughts, questions or feedback please come to our Slack community.

Go try Svix at Svix.com!


For more content like this, make sure to follow us on Twitter, Github or RSS for the latest updates for the Svix webhook service, or join the discussion on our community Slack.