HookDeploy’s payload transformation editor lets you modify webhook payloads before they reach each forward destination. No code, no JSONata — configure rules in the dashboard and preview against a captured request.
Configure transformations per destination in Endpoint → Settings → Forward destinations → Configure transform.
To reshape what HookDeploy stores (without changing what destinations receive), see Privacy filters.
Transformation modes
| Passthrough | Allowlist | |
|---|---|---|
| Default behavior | Forward all fields | Forward only selected fields |
| Best for | Tweaking a few fields on an otherwise complete payload | Strict PII control, analytics pipelines |
| Unselected fields | Included unchanged | Dropped from output |
| Strip PII | Toggle “Strip field” on specific paths | Uncheck fields you don’t want |
| Static fields | Added on top of forwarded payload | Added to allowlisted output |
When to use passthrough
Your webhook sender sends a rich payload and you only need to rename amount to amount_usd, mask an email, or add a source field. Most fields pass through untouched.
When to use allowlist
You’re forwarding to a third-party analytics tool and want to send only event_type and amount — nothing else. Allowlist mode ensures no accidental PII leakage.
Field path syntax
Paths use dot notation to reach nested fields:
amount → top-level field
data.amount → nested object
customer.email → deeply nested
items.0.price → first array element's price field
When you add a field rule, the output path defaults to the full source path (for example data.amount → data.amount). Change Rename to only when you want a different key. Nested allowlist selections keep their nested structure in the output.
In the visual editor, click any field in the JSON tree to add a transform rule. The editor loads your latest captured webhook as sample data so you can preview changes live.
Supported body formats
Transformations run on:
- JSON bodies (
application/jsonand JSON-parsed payloads) - Form-encoded bodies (
application/x-www-form-urlencoded)
Form fields are parsed with standard URL form rules: a key that appears once stays a string; a key that appears more than once becomes an array.
a=1&b=2&b=3 → { "a": "1", "b": ["2", "3"] }
Other content types are not transformed; the original body is forwarded unchanged.
Per-destination output format
Each forward destination has an output format:
| Setting | Behaviour |
|---|---|
| JSON (default) | Encode the transformed object as application/json |
| Keep original format | When the inbound body was form-urlencoded, re-encode a flat result as form-urlencoded |
If you choose Keep original format but the transformed object has nested objects (or other values form encoding cannot represent), HookDeploy falls back to JSON and records an error on the forward result. The transformed data is still delivered — it is not discarded.
Source × output matrix
| Inbound Content-Type | Output format | What is forwarded |
|---|---|---|
| JSON | JSON or Keep original | Transformed JSON |
application/x-www-form-urlencoded | JSON | Transformed object as JSON |
application/x-www-form-urlencoded | Keep original | Form re-encode when the result is flat; otherwise JSON fallback + recorded error |
| Other / empty | either | Transform skipped; original body forwarded |
Conditions
Any field or combine rule can include an Only when… condition. If the condition fails and you have not configured Otherwise… formatters, the rule is skipped (no write to the target path). If Otherwise… is configured, those formatters run instead.
Condition fields
| Field | Description |
|---|---|
| Path | Defaults to the rule’s own source path (or the first combine source). You can point it at any other field. |
| Operator | See table below |
| Value | Compared value — hidden for exists and is_empty |
Operators
| Operator | Passes when |
|---|---|
equals / not_equals | Values compare equal / not equal under the coercion rules below |
contains | Left is a string containing String(right), or an array with an element that equals right |
starts_with / ends_with | Left string starts/ends with String(right) |
greater_than / less_than | Both sides coerce to finite numbers and compare |
exists | Path is present (not undefined) |
is_empty | Value is null, undefined, '', or an empty array/object |
matches | Left string matches a bounded regex pattern |
Type coercion
These rules matter when your condition value is typed differently from the payload field:
equals/not_equals: Same-type values use strict equality. A number and a numeric string can match (42equals"42"). A boolean can match the lowercase strings"true"/"false". Other mixed types do not match via string conversion — for example42does not equal"42px", and an object never equals a string.greater_than/less_than: Both sides run throughNumber(). If either side isNaN, the comparison fails (does not pass).contains: Arrays use the same equality rules asequalsfor each element. Non-string, non-array left values fail.
Combine rules
Use Combine fields to join multiple source paths into one output string, then optionally run formatters and conditions on the result.
| Field | Description |
|---|---|
| Sources | One or more field paths (up to 20) |
| Separator | Inserted between values (default: empty string) |
| Target path | Where the combined string is written |
| Formatters / condition | Same as a field rule |
Missing sources contribute an empty segment and a warning in the transform result.
Error handling
Formatters run independently per rule. If a formatter fails, that rule keeps the value from before the failed formatter and continues with remaining formatters when applicable. Other rules still apply. Errors are listed on the forward result — the destination still receives the partially transformed payload (not the pre-transform original, unless the transform never ran).
Storage privacy filters behave differently: see Privacy filters.
Formatter reference
Formatters run in the order you add them. Chain multiple formatters on one field — for example, cents_to_currency then prefix with value "USD ".
There are 35 built-in formatters. Two older aliases (cents_to_dollars, dollars_to_cents) still run on existing configs but are superseded by the currency formatters below.
Numeric
| Formatter | Options | Input example | Output example | Bad input |
|---|---|---|---|---|
cents_to_currency | currency (ISO 4217, e.g. USD) | 4200 + USD | "42.00" | Missing/unsupported currency or non-number → error; value kept |
currency_to_cents | currency | "42.00" + USD | 4200 | Same |
cents_to_dollars | — (deprecated; prefer cents_to_currency + USD) | 4200 | "$42.00" | Non-number → error |
dollars_to_cents | — (deprecated; prefer currency_to_cents + USD) | 42.00 | 4200 | Non-number → error |
multiply | value (number) | 100 × 1.5 | 150 | Non-number → error |
divide | value (number) | 100 ÷ 4 | 25 | Non-number or divide-by-zero → error |
round | decimals (0–10) | 3.14159 | 3.14 | Non-number → error |
abs | — | -42 | 42 | Non-number → error |
Zero-decimal currencies (for example JPY) and three-decimal currencies (for example KWD) use the engine’s ISO minor-unit rules.
String
| Formatter | Options | Input example | Output example | Bad input |
|---|---|---|---|---|
uppercase | — | "hello" | "HELLO" | Coerced via String |
lowercase | — | "HELLO" | "hello" | Coerced via String |
titlecase | — | "hello world" | "Hello World" | Coerced via String |
trim | — | " hello " | "hello" | Coerced via String |
prefix | value (string) | "123" + "order_" | "order_123" | — |
suffix | value (string) | "123" + "-v2" | "123-v2" | — |
truncate | max (length) | "hello world" max 5 | "hello" | — |
mask | show_chars, position (start/end) | "john@example.com" show 3 start | "joh*************" | — |
hash | — | "john@example.com" | SHA-256 hex digest | Preview shows a placeholder; digest is computed at forward/storage time |
split | separator (default ,) | "a,b,c" | ["a","b","c"] | Always splits String(input) |
join | separator (default "") | ["a","b"] + "-" | "a-b" | Non-array → error |
replace | find, replace_with, regex (bool) | "aa" find a → b | "bb" | Missing find, or invalid/unsafe regex → error. Regex patterns max 256 chars; subjects capped at 10,000 chars for matching |
substring | start (≥0), optional end | "abcdef" start 1 end 4 | "bcd" | Invalid bounds → error |
pad_start | length, pad_string (default " ") | "7" length 3 pad "0" | "007" | Missing/invalid length → error |
pad_end | length, pad_string | "7" length 3 pad "0" | "700" | Same |
Date / time
| Formatter | Options | Input example | Output example | Bad input |
|---|---|---|---|---|
unix_to_iso | — | 1716912060 | "2024-05-28T14:01:00.000Z" | Non-numeric / invalid → error |
unix_to_date | — | 1716912060 | "2024-05-28" | Same |
iso_to_unix | — | "2024-05-28T14:01:00Z" | 1716912060 | Unparseable → error |
format_date | pattern | 1716912060 + YYYY-MM-DD | "2024-05-28" | Missing pattern or invalid date → error |
to_timezone | timezone (IANA, e.g. America/New_York) | ISO/unix + Europe/London | Local wall time YYYY-MM-DDTHH:mm:ss.SSS (no Z) | Missing/invalid zone or date → error |
format_date tokens (UTC): YYYY MM DD HH mm ss SSS. Example pattern: YYYY-MM-DD HH:mm:ss.
Numeric timestamps smaller than 1e12 are treated as Unix seconds; larger values as milliseconds.
Type conversion
| Formatter | Options | Input example | Output example | Bad input |
|---|---|---|---|---|
to_string | — | 42 | "42" | — |
to_number | — | "42.5" | 42.5 | Non-numeric → error |
to_boolean | — | "true" | true | Unrecognised → error |
parse_json | — | "{\"a\":1}" | { "a": 1 } | Invalid JSON → error |
stringify_json | — | { "a": 1 } | "{\"a\":1}" | Unserializable → error |
json_escape | — | hello "x" | Escaped string content | Escape failure → error |
Value override
| Formatter | Options | Input example | Output example | Bad input |
|---|---|---|---|---|
set_value | value (any) | any | your configured value | — |
set_null | — | any | null | — |
default | value (fallback) | "" / null / missing | your fallback | Missing value → error; non-empty input is left unchanged |
Limits
These bounds are enforced when you save a transformation and again when it runs:
| Limit | Value |
|---|---|
| Rules per transformation (field + combine) | 50 |
Formatters per list (formatters or else_formatters) | 20 |
| Sources per combine rule | 20 |
Regex pattern length (matches / replace) | 256 characters |
| Regex subject length (matching only) | 10,000 characters |
Static fields
Add key-value pairs that appear in every forwarded payload regardless of the source webhook. Useful for injecting tenant IDs, source labels, or schema version markers:
{
"source": "hookdeploy",
"tenant_id": "acme-corp"
}
Static fields appear in the live preview in green. They are merged into the transformed output after field rules are applied.
Common recipes
Strip PII before forwarding to analytics
Mode: Passthrough
- Load a sample webhook in the transform editor
- Click
customer.emailin the field tree - Enable Strip field (PII) on the rule card
- Repeat for
customer.phone,customer.name, etc. - Save the transform
Your analytics destination receives the full event minus sensitive customer fields.
Convert Stripe amounts with currency
Mode: Passthrough
- Click
data.object.amount(or your amount field path) - Optionally rename the output path
- Add formatter:
cents_to_currencywith currencyUSD - Preview shows
"42.00"from input4200
See also: Stripe webhooks.
Normalize timestamp formats
Mode: Passthrough
- Click your Unix timestamp field (e.g.
created) - Add formatter:
unix_to_iso - Output becomes ISO 8601 with milliseconds:
"2024-05-28T14:01:00.000Z"
For date-only fields, use unix_to_date. For custom layouts, use format_date.
Mask an email only in live mode
Mode: Passthrough
- Add a rule on
customer.emailwith formattermask - Open Only when…
- Path:
livemode, operator:equals, value:true - Without Otherwise…, test-mode emails pass through unchanged
Allowlist only the fields your server needs
Mode: Allowlist
- Switch mode to Allowlist
- Select the nested paths your server expects — for example
event_type,data.id,data.amount - Add formatters as needed (e.g.
cents_to_currencyondata.amount) - Preview confirms selected fields keep their nested paths unless you rename them
Everything else — including unexpected PII — is dropped.
Transform Twilio or Slack form posts
- Capture a form-urlencoded webhook (Twilio, Slack slash commands, etc.)
- Configure the destination transform against the parsed fields
- Prefer JSON output unless the downstream still requires form encoding
- If you need form output, keep the transform flat (no nested objects)
Live preview
The editor runs your transformation against sample data in real time:
- Unchanged fields — default text color
- Transformed fields — orange/warning color
- Static fields — green/success color
- Removed fields — shown as comments in passthrough mode
- Hash — preview shows a clear placeholder; the real SHA-256 runs at forward time
Load your latest captured request or paste custom JSON to test against Stripe, GitHub, Twilio, or any provider payload. Preview errors mirror production forwarding: failed formatters keep their input value and other rules still apply.
Plan availability
Payload transformation for forwarding is available on Starter and above. Forwarding transformations are not limited by formatter type — every formatter listed here is available once transformations are unlocked.
Privacy filters (storage) use a different plan gate. See Privacy filters.
Next steps
- Privacy filters — Redact or reshape what HookDeploy stores
- Forwarding — Multiple destinations and header injection
- Stripe webhooks — Stripe-specific transformation example
- REST API — Manage destinations programmatically