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.

Image ReservedTransformations dashboard overview/docs/transformations/dashboard-overview.png

When 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:

TS
type TransformationEnvelope = {
  headers: Record<string, unknown>;
  body: unknown;
  query: Record<string, string | string[]>;
  path: string;
  method?: string;
};
FieldMeaning
headersForwardable request headers after FastHook removes internal control headers.
bodyThe parsed JSON body when possible, otherwise the original body value.
queryParsed query string values. Repeated parameters are arrays.
pathThe request path FastHook will use for the outbound delivery.
methodThe 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.

JS
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.

JS
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:

JS
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, or method fall back to the current request
  • provided headers replace the outbound headers after header-safe sanitization
  • provided query replaces the outbound query object after query-safe sanitization
  • provided body replaces the outbound body after JSON-safe sanitization
  • invalid or empty path and method fall back to the current values

Include existing values explicitly when you want to preserve them:

JS
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:

JS
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.

JSON
{
  "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.

Shell
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.

JSON
{
  "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
  }
}
Image ReservedTransformation run editor/docs/transformations/run-editor.png

Attaching To A Connection

Transformations run from connection rules. A connection rule can reference a saved transformation by transformation_id.

JSON
{
  "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:

  1. deduplicate
  2. transform
  3. filter
  4. delay
  5. retry

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.

Image ReservedConnection transform rule/docs/transformations/connection-rule.png

Runtime 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_id
  • webhook_id, which is the connection id
  • transformation_id
  • original_event_data_id
  • transformed_event_data_id
  • original_event_data
  • transformed_event_data
  • log_level
  • logs
  • created_at and updated_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:

Shell
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:

Shell
curl "/v1/transformations/trs_.../executions/tex_..." \
  -H "Authorization: Bearer fhp_YOUR_PROJECT_API_KEY"
Image ReservedExecution history/docs/transformations/execution-history.png

Common Recipes

Add a destination API key

JS
addHandler("transform", (request, context) => ({
  ...request,
  headers: {
    ...request.headers,
    "authorization": `Bearer ${context.env.API_KEY}`
  }
}));

Rename body fields

JS
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

JS
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

JS
addHandler("transform", (request) => {
  const body = { ...request.body };
  delete body.card_number;
  delete body.raw_provider_payload;

  return {
    ...request,
    body
  };
});

Change path and method

JS
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.