Transforms and custom responses
Two kinds of script can run on each request, both edited in the endpoint’s Transform tab:
- A transform changes the request before it’s forwarded, or skips it.
- A response handler decides the status, headers and body returned to the webhook sender.
Scripts are JavaScript. They run sandboxed in WebAssembly (QuickJS), on the server in server mode and in your browser tab in browser mode. Both load the same helpers, so a script behaves the same in either mode and in the editor’s Test button.
- Each run gets a fresh runtime, so nothing carries over between requests.
- A script has 5 seconds to finish.
- On the server, memory is capped at 64 MB and the result at 1 MB.
Writing and trying a script
Section titled “Writing and trying a script”The endpoint page shows the pipeline at the top: Capture → Transform → Forward → Reply. Click Edit on Transform, or Change on Reply, to open its editor. Test a transform on this on any request opens the transform editor with that request loaded.
The editor has three columns:
- Input: pick what to try it on (a captured request, a sample, or your own request) and see the
reqyour script gets. Edit lets you change the method, path, headers or body. - Your transform (or Your reply): the script, with Start from examples above it.
- What will be forwarded (or What the sender gets): updates as you type, with a line diff of the body for a transform.
Below, Check it against more traffic runs the script on recent captured requests and points out any it throws on. Nothing is sent while you try a script, and nothing is saved until you click Save. Untick Run on every request to keep a script saved without running it.
What a script gets
Section titled “What a script gets”Both kinds of script receive the same req:
req.method // "POST"req.path // "/stripe/events", the part after /h/<slug>req.query // { debug: ["1"] }, values are arraysreq.headers // { "Content-Type": ["application/json"] }, values are arraysreq.body // the raw body, always a stringreq.json // the body parsed as JSON, or null when it isn't JSONreq.content_type // "application/json"
req.header("x-event") // first value of a header, any case; undefined if absentreq.setHeader("x-source", "test") // replace a headerreq.removeHeader("x-signature") // delete a headerTransforms
Section titled “Transforms”Define transform(req) and return the request to forward:
function transform(req) { if (req.json) { req.json.received_at = new Date().toISOString(); delete req.json.internal_id; } return req;}- Changes to
req.jsonare written back to the body. If you don’t touchreq.json, the body is forwarded byte for byte. - You can also set
req.bodyyourself, or return a new object. Fields you leave out keep their original values. - Setting
req.jsonon a request that wasn’t JSON sets the content type toapplication/json, unless you set one. - Changing the body breaks a signed sender’s signature. To keep your app’s signature check passing, turn on re-signing: the forwarded request is signed again with the same secret after the transform.
Skipping a request
Section titled “Skipping a request”Return null to capture a request without forwarding it. The sender gets the default reply; the custom reply doesn’t run for a skipped request.
function transform(req) { if (req.json?.type === 'ping') return null; return req;}When something goes wrong
Section titled “When something goes wrong”If a transform throws, times out or returns nothing, the original request is forwarded unchanged and the error is logged. Forgetting return req is the most common mistake; the Test button shows it as an error.
Scripts from older versions
Section titled “Scripts from older versions”Earlier versions also ran Lua and Jsonnet in browser mode. Those scripts are kept with the endpoint but no longer run. The Transform tab shows them read-only with a button to start a JavaScript version.
Custom responses
Section titled “Custom responses”Enable the response handler and define handler(req). It runs after the transform and the forward, and returns the response for the sender:
function handler(req) { return { status: 200, headers: { "X-Powered-By": "Testhooks" }, body: JSON.stringify({ ok: true, received: req.method }) };}statusdefaults to200.headersis a flat object of strings.bodyis a string, empty by default.content_typedefaults totext/plain.
Return null to fall back to the default response. If the handler fails, the default response is used.
Use it to test how your sender copes with errors, without changing your app:
function handler(req) { if (req.query.fail) return { status: 500, body: "simulated failure" }; return { status: 200, body: "ok" };}Using the forward target’s response
Section titled “Using the forward target’s response”With sync forwarding, the target’s response is available as req.forward_response:
function handler(req) { const upstream = req.forward_response; // { status, headers, body, content_type } if (upstream.status >= 500) { return { status: 202, body: "queued for retry" }; } return { status: upstream.status, body: upstream.body, content_type: upstream.content_type };}Headers you can’t set
Section titled “Headers you can’t set”For safety, scripts can’t set security-sensitive response headers. These are ignored:
Set-Cookie, Content-Security-Policy, Content-Security-Policy-Report-Only, X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security, Permissions-Policy, Referrer-Policy, X-XSS-Protection, Access-Control-Allow-Origin, Access-Control-Allow-Credentials and Transfer-Encoding.