# Payload Transforms

Payload transforms let you reshape the outbound JSON body without changing the original accepted source payload. This is the right layer when the producer format is fixed but the consumer wants a narrower or cleaner JSON contract.

## Transform model

Hooksbase supports one saved transform per webhook:

- `jsonpath` with an `expression`
- `handlebars` with a JSON-rendering `template`

Non-null transforms require Starter+.

This surface is intentionally constrained:

- JSON input only
- JSON output only
- no JavaScript execution
- no multi-step pipelines
- no request method, URL, or header mutation

## Auth model

- **Public API** `project API key`: Use `GET/PUT /v1/webhooks/{id}/transform` to inspect, replace, or clear the saved transform.
- **Dashboard** `session auth`: The [dashboard](/docs/dashboard.md) lets you author, preview, and save a transform from the webhook's transform tab.
- **SDK / CLI**: The first-party SDK and CLI do not wrap transform management yet. Use raw HTTP.

## JSONPath and Handlebars

JSONPath is the lighter option when you only need to extract or project part of the source payload. The `expression` is evaluated against the parsed source JSON; the result is serialized back out as the outbound body.

Handlebars is the better fit when you need to build a new JSON object from scratch. The template context exposed to the template is:

- `payload` — the parsed JSON body
- `headers` — the inbound headers (lower-cased)
- `contentType` — the inbound `Content-Type`

The only helper available today is `json`, which lets you embed objects and arrays without string escaping. The rendered output must parse as JSON, or the transform returns `422`.

**Save a Handlebars transform**

```bash
curl https://api.hooksbase.com/v1/webhooks/wh_123/transform \
  -X PUT \
  -H "Authorization: Bearer swk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "transform": {
      "kind": "handlebars",
      "template": "{\"tenant\":\"{{headers.x-tenant}}\",\"customer\":{{json payload.customer}}}"
    }
  }'
```

Clearing with `{ "transform": null }` is allowed on every tier.

## Execution and replay behavior

Execution order matters:

1. ingest validation
2. routing on the original source payload
3. transform execution
4. source payload storage
5. transformed dispatch snapshot storage
6. delivery persistence

Operational consequences:

- non-JSON input, invalid JSON input, JSONPath evaluation failures, or Handlebars output that does not parse as JSON return `422`
- downgraded projects with a still-saved transform return `403` until the transform is cleared or the project is upgraded
- retries, replay, DLQ re-drive, and bulk replay use the saved transformed dispatch snapshot when present, so replay stays deterministic after config changes

## Related routes

- `GET /v1/webhooks/{id}/transform`
- `PUT /v1/webhooks/{id}/transform`

## Common mistakes

- Expecting routing to see the transformed payload. Routing always evaluates the original source payload.
- Returning non-JSON output from a Handlebars template.
- Treating transforms as a free-tier feature. Non-null saves start at Starter.
- Assuming replay will re-run the current transform instead of using the saved dispatch snapshot.
