Webhook changelogs: the breaking change nobody requested
5 min read
A REST API changelog exists because a caller can choose to reject a response it does not understand, or at least log an error loud enough that someone notices. A webhook receiver rarely does either. It gets a POST, reads the fields it expects, and if a field moved, changed type or disappeared, the endpoint either crashes quietly in a background job nobody watches or, worse, keeps running with a wrong value it never validated. What is a breaking change covers the general definition; a webhook payload needs its own answer, because the failure mode is different from an endpoint someone calls on purpose.
Why does a webhook payload change break differently than an API response change?
Because the direction of the request is reversed. A REST caller initiates the call and can add a version header, retry on a 4xx, or read a deprecation notice in the response. A webhook receiver did none of the initiating: your server decided to send, decided when, and decided what shape the body would take. The receiver’s only leverage is whatever validation it wrote when the integration was built, and most integrations are built once, working, and never revisited until they break. That asymmetry is the whole reason a webhook payload change deserves more caution than the equivalent change in a response body a caller actively requested.
What actually counts as a breaking change in a webhook payload?
| Change | Breaking for most receivers |
|---|---|
| Adding a new field | No, if receivers ignore unknown fields (verify this assumption, do not assume it) |
| Removing a field | Yes, if anything reads it |
| Renaming a field | Yes, functionally identical to removing the old one |
| Changing a field’s type (string to object) | Yes, almost always |
| Reordering fields in the JSON body | No, for any receiver parsing by key, which should be all of them |
| Changing the event name or type string | Yes, if receivers filter or route on it |
The “adding a field is safe” row is the one teams lean on hardest and the one worth verifying, not assuming. A permissive JSON parser ignores unknown fields by default, but a receiver that deserializes into a strict schema, several typed languages do this without extra configuration, can reject the whole payload the moment an unexpected field shows up. Adding a field is safe for your webhook only if you know something about how receivers parse, not because JSON itself is forgiving.
How do you version a webhook payload?
Much as for an API response, with one twist: the receiver never sends a request, so it cannot ask
for a version, and the sender has to state it. That can go in the body or in a request header on
the delivery itself; GitHub’s deliveries
carry X-GitHub-Event and X-GitHub-Hook-ID, and the
Standard Webhooks spec
puts its metadata in webhook-* headers. A version field on
the payload itself ("payload_version": 2) is the cheapest option and works when receivers are
willing to branch on it. A versioned event type (invoice.updated becomes invoice.updated.v2 as
a distinct event a receiver opts into) is more work to build but means the old shape keeps flowing
to anyone who never migrated, which matters more here than for a REST endpoint because you cannot
phone every receiver to tell them to update. A per-subscription setting, chosen when the webhook
endpoint is registered, front-loads the decision instead of branching on every delivery, and is the
right choice when you already have a subscription record to attach it to.
POST /receiver-endpoint
{
"event": "invoice.updated",
"payload_version": 2,
"data": { "invoice_id": "inv_123", "status": "paid" }
}
How do you even know who is listening?
Worse than an API changelog’s version of the same problem, because a webhook has no incoming request log on your side that names the caller; you only have your own outgoing delivery log, which tells you an endpoint received a 200, not what it did with the body. Track two things at minimum: every registered endpoint URL with an owner, the same discipline internal API changelogs recommend for internal consumers, and your delivery failure rate per endpoint after a payload change ships. A spike in 4xx or 5xx responses from one endpoint right after a change is the closest thing to a stack trace you will get, and it is often the only signal that a receiver broke, since the team running it may not notice for days.
Should a webhook changelog be separate from the API changelog?
A separate section on the same page, not a separate publication. An API changelog already establishes who reads it and how they subscribe; a webhook payload change belongs in the same feed, tagged distinctly enough that a receiver-side engineer scanning for “does this affect my integration” can filter to it, because a webhook consumer often has no other reason to check a general API changelog and will only find it if someone links them there directly.
What does a reasonable deprecation window look like for a webhook payload?
Longer than the equivalent REST deprecation, because migration on the receiving end usually means a
second team, one you may not have a direct line to, has to notice, schedule, and ship a fix with no
urgency of their own. A month is a reasonable floor for a payload the receiver could plausibly still
be parsing with a permissive library; three months or more is safer for a field removal that a
strict schema would reject outright. Send the old and new shapes together during the window when
that is feasible (the old status field and its version 2 replacement in the same payload),
because a receiver that reads the old field keeps working without touching their code, and one
that has already migrated simply ignores the field it no longer needs.
FAQ
Do webhook consumers need to acknowledge a payload change before it ships? No acknowledgment mechanism exists by default, which is exactly why the deprecation window matters more here than for a REST API: nobody confirms readiness, so the window has to be long enough that most receivers migrate on their own schedule before the old shape disappears.
Should unknown fields ever be treated as safe to add without notice? Only once you have verified, not assumed, that your receivers parse permissively. A changelog entry costs little and removes the guesswork; silently adding fields on the assumption that “JSON parsers ignore extras” breaks any receiver using strict deserialization.
What is the fastest way to detect a broken webhook receiver after a payload change? A per-endpoint delivery failure rate, watched in the hours right after the change ships. It will not tell you what broke, only that something did, but it is the earliest and often the only signal you get.
Does retry logic help receivers survive a payload change? No. A retry resends the same new payload; it does not revert to a shape the receiver can parse. A payload change breaks a receiver on the first delivery and every retry after it identically.
The technical claims in this article have not been independently reviewed. If something here is wrong, tell us and we will correct it.