# Webhook versioning: the version your consumers never see

> You version the event. They receive a type, a timestamp and a payload. Which changes break them, which ones are silent, and why the fix is rarely a v2.

- Author: Quentin Mousset
- Published: 2026-09-03
- Canonical: https://repost.sh/blog/webhook-versioning-consumers-never-see

![Two blocks on black joined by a wide arrow. Left: three stacked migration cards, v1 add order.created, v2 add currency, v3 rename reference, gathered by a brace reading your git history. The arrow is annotated in coral, none of this crosses. Right: a single JSON box holding type order.created, a timestamp and data, with a coral magnifying glass over it hunting for a field called version, next to a coral question mark. Under a dashed line: adding is safe, renaming is silent.](/images/blog/figure-webhook-versioning.webp)

You shipped <code>order.created</code> eight months ago. Three customers built against it, and one of
  them has not touched their handler since. Now you need the payload to carry a currency. The question
  everyone asks first is how to version the event. It is the wrong question, and answering it will not
  tell you whether those three customers survive Tuesday.

## The version is not in the envelope

Here is what actually lands on a consumer's endpoint:

```json
{
  "type": "order.created",
  "timestamp": "2026-07-17T12:00:00.000Z",
  "data": { "id": "ord_1", "amount": 42.5 }
}
```

A type, a time, a payload. The headers alongside it carry an id, a timestamp
and a signature ([the full envelope](/docs/send/signing)). Nowhere in there is
a version number.

So whatever you record on your side, the consumer cannot branch on it. They
parse the shape in front of them and hope it is the shape they wrote code for.
Versioning is real, and it is a governance tool for you: a record of what
changed, when, and by whom. It is not a contract term they can read at runtime.

Which means the useful question is not "how do I version this". It is **which
of my changes are visible to them, and which of those are silent**.

## What each change actually does to them

```prisma
model Order {
  id       String
  amount   Float
  currency String?
}
```

| What you change | What they see | What breaks |
|---|---|---|
| Add an optional field | One extra key | Nothing, unless they reject unknown keys |
| Add a required field | One extra key | Same as above. Required is your problem, not theirs |
| Rename with `@map` | The old key vanishes, a new one appears | Their reads return undefined. **Silent** |
| Remove a field | The key vanishes | Same. **Silent** |
| Change a type | Same key, different JSON type | A parse error if you are lucky, a coercion if you are not |

Two rows carry the danger, and they are the two that produce no error on your
side at all. A consumer reading `data.currency` on a payload that no longer has
it gets `undefined`, passes it to something that accepts `undefined`, and
writes a row you will hear about in a month.

Adding is safe. Removing and renaming are quiet. That asymmetry is why the
answer to most versioning questions turns out to be "add a field, and leave the
old one alone".

> **Absent is not null, and that is useful**: An optional field has three states the SDKs keep apart: set, absent, or explicitly null. A field that is absent is simply omitted from the payload unless it declares a `@default` ([field modifiers](/docs/send/schema)). So a consumer can tell "this order has no currency" from "this event predates currency", which is exactly the distinction an additive change needs them to make.

## What the wire name buys you

`@map` decides the key in the payload, independently of the name in your code:

```prisma
model Order {
  reference String @map("order_reference")
}
```

Your code reads `order.reference`, the payload carries `order_reference`. That
indirection is worth understanding in both directions. It lets you rename
things internally without touching a single consumer, which is the cheap
refactor. And it means changing the mapped name is a breaking change dressed as
a rename, which is the expensive one. The compiler will not stop you, because
from its point of view nothing happened.

## Where versioning does earn its place

Not at runtime, but in review. Schema changes are recorded as migrations in
your repo, the way database migrations are
([migrations and deploys](/docs/send/deployments)):

```bash
repost schema migrate dev --name add_currency
```

The migration is committed, so the contract change shows up in `git diff` next
to [the clients it regenerates](/blog/one-webhook-schema-six-sdks). A reviewer sees the change and its blast
radius on one screen, which is the moment a silent rename is still cheap to
catch.

Two guarantees are worth knowing because they change what you can do under
pressure. The server refuses any deploy whose history does not descend from
what is already deployed, so production history cannot be lost by accident.
And removing event types is not something you do by omission: a migration that
would drop them lists them and asks for an explicit confirmation, and errors in
CI unless you pass a flag that says you meant it.

## Retiring an event is a social problem

The technical half is handled, and in a way most people assume it is not.
Removing an event type from your schema **archives** it rather than deleting
it. An archived type still accepts publishes and still delivers. A wrong deploy
does not drop traffic that is in flight, and redeploying the previous head
restores the registry.

That protects you from the accident. It does nothing about the customer whose
integration quietly stops receiving the event they built a workflow on, because
nothing in the delivery layer can tell them. The only mechanism that works
there is the one that has always worked: tell them, keep sending for a stated
period, and watch who is still consuming it before you actually stop.

## What none of this fixes

**Their deploy schedule.** You can version perfectly, generate clients in six
languages, and publish [docs that rebuild themselves on every
deploy](/docs/send/event-docs). The customer who has not touched their handler
in eight months still has not touched it. A v2 event type is not a
migration, it is a second thing to maintain until they move, and some of them
never will.

**Strict consumers.** If someone validates your payload with a schema that
rejects unknown keys, your safest change (adding an optional field) breaks
them. There is nothing you can do about that from your side except know it
exists, which is a good reason to say in your docs that you add fields without
notice.

The rule that survives all of it: **treat the payload as append-only, and treat
removal as a conversation rather than a deploy.** Everything else is
bookkeeping, and bookkeeping is for you, not for them.

**See it on your own events.** Open a live inbox and inspect your first webhook in seconds (50K events/month free).
