The reshaping you keep rewriting in every handler
Every provider you integrate ships a different payload shape. Stripe wraps the object in a data.object envelope. GitHub puts the action at the top level and the resource beside it. A billing vendor sends amount_cents, a tax vendor sends total as a string with a currency code glued on. Your handlers spend their first twenty lines doing the same thing: digging the fields you care about out of a structure someone else designed, coercing types, renaming keys so the rest of your code sees one consistent internal event.
That reshaping code multiplies. Ten providers, ten adapters. Each has its own quirks and its own place to break. So teams go looking for somewhere central to do it once, and the delivery layer puts its hand up. Svix, Hookdeck, and most webhook gateways now let you transform the payload before it reaches your handler. It sounds like a clean win. It is also where a whole new category of quiet failure moves in.
What a gateway transformation actually is
A transformation is code that runs on the payload in transit, between the sender and your endpoint, hosted by the delivery layer instead of by you. Hookdeck runs JavaScript on inbound events before delivery to your destination, handing your function a request object with headers, body, query, and path that you mutate and return. Svix, which sits on the sending side, shipped a revamped Transformations UI and Transformation Variables in July 2026, the latter letting you pull configuration out of hardcoded transform code.
// A gateway transform, running on someone else's infrastructure
function handler(request) {
const body = request.body
request.body = {
type: body.event_type,
id: body.data.id,
amount: body.data.amount_cents / 100,
occurredAt: body.created,
}
return request
}
The appeal is obvious. Your handler stops caring which provider sent the event because everything arrives in your shape. The reshaping lives in one place. What the diagram hides is that the delivery layer has just rewritten the bytes your endpoint receives, and some of those bytes were load-bearing.
The signature you just invalidated
Here is the part that catches teams out. The provider computed an HMAC over the exact body it sent. Your verification recomputes that HMAC over the exact body you received, then compares the two. The whole thing only works because both sides hash the same bytes.
A transform changes the bytes. The moment the gateway rewrites amount_cents into amount, reorders keys, or re-serializes the JSON with different whitespace, the body your handler sees no longer matches the signature the provider signed. Recompute the HMAC now and it will never match, because you are hashing a document the sender never saw.
Teams discover this the fast way: verification starts failing on every event the instant they enable a transform. The dangerous response is the obvious one. Somebody decides verification is the problem, wraps it in a try/catch that logs and continues, or comments it out entirely. Now your endpoint accepts any payload from anyone, which is the exact hole signature verification existed to close. You did not lose a security feature to an attacker. You switched it off yourself to make a convenience feature work.
Verify first, transform second
The fix is ordering, and there are only a few honest arrangements. The cleanest is to verify before you transform. The gateway holds the signing secret, checks the signature against the original untouched body, and only then reshapes the payload for delivery. Your handler trusts the gateway instead of the provider, which means the gateway is now inside your security boundary and you had better trust it accordingly.
The alternative is to keep verification in your own code and refuse to let anything upstream touch the signed body. The gateway can add headers, route, retry, and log, but the body that reaches your HMAC check must be byte-identical to what the sender signed. If you want a normalized shape too, transform after verification, inside your handler, on a copy.
// In your handler: verify the raw body, then reshape
const raw = await readRawBody(req) // untouched bytes
verifyHmac(raw, req.headers['webhook-signature']) // throws on mismatch
const event = normalize(JSON.parse(raw)) // reshape a copy, post-verify
What you cannot do is verify a body that has already been rewritten and pretend the check still means something. A green checkmark on a transformed payload verifies that the gateway can talk to itself. It says nothing about the sender.
Redacting PII before it reaches your logs
There is one job the gateway does that your handler genuinely cannot. If a payload carries a full card holder name, a national ID, or a raw email, that data lands in your ingress logs, your retry queue, and your dead-letter storage the instant it arrives, long before your code decides to be careful with it. Redacting at the gateway is the only place you can strip a field before it is ever written down on your side.
This is the one transformation worth having in the delivery layer even though it fights the signature problem, and it fights it hard. You want the raw signed body preserved long enough to verify, and you want the sensitive field gone before anything persists it. The way through is to verify first, then redact for storage and logging while keeping the verified original in memory only for the length of the request. Treat redaction as a storage-and-observability concern, not a correctness one, and keep it far away from the bytes your HMAC check reads. Your structured logs should never have been the place a customer's data came to rest.
Transforms are untested code wearing a UI
Open a gateway's transformation editor and look at what you are actually doing. You are writing JavaScript, in a text box, on a vendor's website, that runs in production against every event. It has no unit tests. It is not in your repository. It went through no pull request. Nobody reviewed the diff, because there is no diff. The rollback story is whatever version history the vendor decided to keep.
This is real code with real branches and real edge cases, and it lives in the one place your engineering discipline does not reach. A transform that assumes data.customer is always present throws the day a provider omits it for guest checkouts. You did not catch that in review, because there was no review, and CI never saw it, because the code is not in CI. Every argument you would make against untested production code applies here with extra force. The failure stays invisible until an event hits the branch you got wrong.
When a transform silently drops the event
A transform that throws is worse than a handler that throws, because of where it sits. When your handler errors, the sender sees a non-2xx response and retries, and your monitoring on the receiving side lights up. When a transform errors inside the gateway, the event may never reach your handler at all. There is nothing on your side to alert on, because from your endpoint's perspective the event simply never existed.
The same trap hides in the return value. A transform that returns null, undefined, or an empty object on a code path you did not think about can drop the event on the floor, and the gateway happily reports the delivery as handled. You end up with the worst version of a silent failure: one that happens before your instrumentation can see it. If you run transforms, you have to monitor the gateway's own transform-error and delivery metrics as first-class signals, not glance at them in a dashboard once a quarter. The layer you cannot see is the layer most able to lose your data.
Filtering is not transformation
There is a strong temptation to use a transform to drop events you do not want. A push event on a branch you ignore, a charge.updated that only touched a field you do not track. It is tempting to return early and make the noise disappear. Resist it. Filtering and transformation are different jobs, and good gateways keep them separate for a reason. Hookdeck, for instance, treats filters as a distinct feature from transformations.
The distinction matters operationally. A filter that drops an event is a routing decision you can reason about and audit: this connection does not receive that event type. A transform that drops an event by returning nothing is an accident wearing the costume of a feature, indistinguishable from the bug where a transform silently fails. Keep the "should this event be delivered at all" decision in a filter, where it is explicit and visible, and keep transforms to the narrow job of reshaping events that are already going to arrive. Mixing the two means you can never tell a deliberate drop from a broken one.
Versioning transforms against a moving payload
Provider payloads change. New fields appear, structures get nested, an API version bump renames the field your transform reaches for. When the payload moves, the transform that mapped it is now wrong, and it is wrong in the place hardest to notice: it keeps returning a valid-looking object, just with a stale or missing field, and every downstream system trusts it.
Your handler code at least gets deployed alongside your other changes, tested against fixtures, versioned in git next to the schema it expects. A transform pinned in a vendor UI drifts on its own timeline. Nobody bumps it when the provider announces a new payload version, because it is not in the changelog anyone reads. If you run transforms at all, give them the same version discipline as the payloads they consume: a fixture of the current provider shape, a test that the transform produces your internal shape, and someone who owns keeping both in step. Transformation Variables help by pulling config out of the code, but they do not make the code itself testable.
Where the transform actually belongs
Strip the convenience away and a pattern falls out. Redaction of sensitive fields belongs at the gateway, because that is the only place early enough to matter, and you accept the signature dance as the cost. Everything else, the renaming and reshaping and type coercion that made the gateway tempting in the first place, belongs in your own code, verified against the raw body, tested against fixtures, versioned in your repository.
The webhook gateway is a real and useful pattern for routing, retries, and cross-cutting concerns. It stops being useful the moment it becomes the place your business logic quietly lives, untested and unversioned, one provider payload change away from silently corrupting every event. Use it for the jobs only it can do. Keep the reshaping where your engineering practices can still reach it. The normalized internal event is worth having. It is not worth hiding in a text box on someone else's website.
Frequently asked questions
Why does signature verification fail as soon as I enable a payload transform? The provider computes its signature over the exact bytes it sent, and verification recomputes that hash over the bytes you received. A transform rewrites those bytes, so the two no longer match. Verify the original untouched body before anything reshapes it, or keep the signed body byte-identical until after your HMAC check runs.
Is it safe to redact PII in a gateway transformation? Redaction is the one transform genuinely worth doing at the gateway, because it is the only layer early enough to strip a field before it lands in your logs, retry queue, and dead-letter storage. The catch is ordering: verify the signature against the raw body first, then redact for storage while keeping the verified original in memory only for the length of the request.
How do I stop a transform from silently dropping events? A transform that throws or returns an empty value inside the gateway can discard an event before it ever reaches your handler, with nothing on your side to alert on. Treat the gateway's transform-error and delivery metrics as first-class monitoring signals, and add explicit fixtures so a transform that returns nothing on an unexpected code path is caught before production.
Should I filter unwanted events with a transform? No. Use a dedicated filter for the decision of whether an event should be delivered at all, and keep transforms to reshaping events that are already going to arrive. A deliberate drop in a filter is auditable and visible, while a drop hidden in transform logic is indistinguishable from a bug.
Where should webhook reshaping code actually live? Keep renaming, type coercion, and envelope-flattening in your own code, verified against the raw body, tested against fixtures, and versioned in your repository. Reserve the gateway for jobs only it can do early enough to matter, such as redaction, and for routing and retries. Business logic pinned in a vendor UI drifts without tests, review, or a real rollback path.