Transformations
Transformations let you modify a webhook request before FastHook delivers it to a destination.
Use them when the source payload and the destination contract do not line up: rename fields, add headers, normalize nested objects, remove noisy data, change the outbound path, or shape a provider-specific event into the format your internal service expects.
In FastHook, transformations are saved JavaScript snippets. A connection can reference one or more saved transformations through transform rules. During delivery, FastHook loads the transformation, runs it against the request envelope, stores the transformed payload, and records an execution audit entry.
/docs/transformations/dashboard-overview.pngWhen To Use A Transformation
Use a transformation when the webhook should still be delivered, but the outbound request needs a different shape.
- Add authentication or routing headers expected by the destination.
- Rename fields from provider terminology to internal terminology.
- Flatten deeply nested provider payloads into a compact event body.
- Remove fields that downstream systems should not receive.
- Convert query parameters, path, or method for a destination endpoint.
- Add derived values such as totals, flags, or normalized identifiers.
Use a Filter instead when the event should not be delivered at all. Filters decide whether an event continues. Transformations change the event that continues.
Request Envelope
A transformation receives a request envelope with five fields:
type TransformationEnvelope = {
headers: Record<string, unknown>;
body: unknown;
query: Record<string, string | string[]>;
path: string;
method?: string;
};| Field | Meaning |
|---|---|
headers | Forwardable request headers after FastHook removes internal control headers. |
body | The parsed JSON body when possible, otherwise the original body value. |
query | Parsed query string values. Repeated parameters are arrays. |
path | The request path FastHook will use for the outbound delivery. |
method | The outbound HTTP method. It is preserved unless you replace it. |
Basic Script Shape
Register a transform handler with addHandler. The handler receives the current request envelope and a context object.
addHandler("transform", (request, context) => {
return {
...request,
headers: {
...request.headers,
"x-api-key": context.env.API_KEY
},
body: {
...request.body,
transformed: true
}
};
});The same environment values are also exposed as process.env.
addHandler("transform", (request) => ({
...request,
headers: {
...request.headers,
"x-tenant": process.env.TENANT_ID
}
}));If several transform handlers are registered inside one script, FastHook uses the last registered handler.
Return Values
The safest pattern is to return the full request envelope:
addHandler("transform", (request) => ({
headers: request.headers,
body: {
event: request.body.type,
payload: request.body.data
},
query: request.query,
path: request.path,
method: request.method
}));FastHook normalizes the returned value before delivery:
- missing
headers,query,path, ormethodfall back to the current request - provided
headersreplace the outbound headers after header-safe sanitization - provided
queryreplaces the outbound query object after query-safe sanitization - provided
bodyreplaces the outbound body after JSON-safe sanitization - invalid or empty
pathandmethodfall back to the current values
Include existing values explicitly when you want to preserve them:
addHandler("transform", (request) => ({
...request,
body: {
...request.body,
normalized: true,
original_type: request.body.type
}
}));Mutating The Body
The dynamic runtime gives the handler a cloned request. You can mutate the body clone and return the request:
addHandler("transform", (request) => {
request.body.received_by = "fasthook";
request.path = "/normalized-webhook";
return request;
});Return a new envelope when changing headers or query. That keeps the script easy to read, easy to test, and predictable during replay.
Environment Values
Each transformation has optional environment JSON. FastHook stores it as env_json and exposes string values to the script.
{
"API_KEY": "fh_live_...",
"TENANT_ID": "acme"
}Use environment values for secrets, destination-specific constants, or reusable configuration. Do not hardcode secrets in the script body.
Only string values are exposed. Non-string values in environment JSON are ignored by the runtime.
Testing A Transformation
Use the dashboard editor or call PUT /v1/transformations/run with inline code, a saved transformation_id, or both.
curl -X PUT "/v1/transformations/run" \
-H "Authorization: Bearer fhp_YOUR_PROJECT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "addHandler(\"transform\", (request) => ({ ...request, body: { ...request.body, normalized: true } }));",
"env": {},
"request": {
"method": "POST",
"path": "/stripe/webhook",
"query": "mode=live",
"headers": {
"content-type": "application/json"
},
"body": {
"type": "invoice.paid"
}
}
}'The run endpoint returns the transformed request, captured console lines, the resulting log level, and timing metadata.
{
"log_level": "info",
"console": [],
"request": {
"headers": {
"content-type": "application/json",
"content-length": "23"
},
"body": {
"type": "invoice.paid",
"normalized": true
},
"query": {
"mode": "live"
},
"path": "/stripe/webhook",
"method": "POST"
},
"transformation_run_time": {
"started_at": "2026-05-16T13:00:00.000Z",
"finished_at": "2026-05-16T13:00:00.004Z",
"duration_ms": 4
}
}/docs/transformations/run-editor.pngAttaching To A Connection
Transformations run from connection rules. A connection rule can reference a saved transformation by transformation_id.
{
"type": "transform",
"transformation_id": "trs_..."
}The legacy rule type transformation is normalized to transform.
When you create or update a connection, FastHook normalizes rules in this order:
deduplicatetransformfilterdelayretry
That means transformation rules run before filter rules in the normalized connection pipeline. If your filter should inspect the original source payload, keep that in mind and put the filtering condition in a place that matches the actual normalized rule behavior.
/docs/transformations/connection-rule.pngRuntime Guidance
FastHook transformations are designed for request shaping, not long-running business workflows. Keep scripts focused, deterministic, and limited to the request data and environment values they need.
Execution History
Every successful transformation in a live connection produces a transformed event data record and queues an execution audit write.
The execution record stores:
event_idwebhook_id, which is the connection idtransformation_idoriginal_event_data_idtransformed_event_data_idoriginal_event_datatransformed_event_datalog_levellogscreated_atandupdated_at
FastHook persists error-level console logs with the execution. Informational logs are returned by manual test runs, but live execution history only stores logs when the transformation produced an error log level.
List recent executions:
curl "/v1/transformations/trs_.../executions?limit=20&dir=desc" \
-H "Authorization: Bearer fhp_YOUR_PROJECT_API_KEY"Retrieve one execution with original and transformed payloads:
curl "/v1/transformations/trs_.../executions/tex_..." \
-H "Authorization: Bearer fhp_YOUR_PROJECT_API_KEY"/docs/transformations/execution-history.pngCommon Recipes
Add a destination API key
addHandler("transform", (request, context) => ({
...request,
headers: {
...request.headers,
"authorization": `Bearer ${context.env.API_KEY}`
}
}));Rename body fields
addHandler("transform", (request) => ({
...request,
body: {
event_type: request.body.type,
object_id: request.body.data?.object?.id,
received_at: new Date().toISOString()
}
}));Use fixed values or source timestamps instead of new Date() when deterministic replay output matters.
Flatten a provider payload
addHandler("transform", (request) => {
const invoice = request.body.data.object;
return {
...request,
body: {
id: invoice.id,
customer_id: invoice.customer,
total: invoice.amount_paid,
currency: invoice.currency,
status: invoice.status
}
};
});Remove sensitive fields
addHandler("transform", (request) => {
const body = { ...request.body };
delete body.card_number;
delete body.raw_provider_payload;
return {
...request,
body
};
});Change path and method
addHandler("transform", (request) => ({
...request,
path: "/internal/webhooks/billing",
method: "POST"
}));Design Tips
- Keep each transformation focused on request shaping.
- Prefer returning a new request object over mutating deeply nested state.
- Preserve fields that downstream systems already rely on.
- Use lowercase header names for consistency.
- Keep secrets in environment JSON.
- Test with real provider samples before attaching to production connections.
- Check execution history when a destination receives a surprising payload.
- Use transformations for operational compatibility, not long-running business decisions.
Common Questions
Do transformations run before delivery?
Yes. A transformation runs in the connection worker before FastHook queues the outbound destination delivery.
Can one connection run multiple transformations?
Yes. A connection can contain multiple transform rules. They run in normalized rule order, and each transformation receives the request produced by the previous transformation.
What happens when a transformation is missing?
The connection event is marked failed with a transformation_not_found failure message, and the destination delivery is skipped.
What happens when transformation code throws?
The connection worker treats it as transformation_execution_failed, records the failure on the event flow, and does not send the broken transformed payload to the destination.
Are transformation executions stored?
Yes. FastHook stores original and transformed event data ids, the original and transformed envelopes, timestamps, and error-level logs for live execution history.