Typeform fires a form_response webhook the moment someone submits a form. That's great in production, but it's miserable to develop against. Typeform needs a public HTTPS URL, so your handler running on localhost:3000 is invisible to it. The usual workarounds—deploying half-baked code to a staging box, or pasting sample JSON into your tests and hoping it matches reality—both waste time and hide bugs. Worse, if you enabled a signing secret, you can't verify the Typeform-Signature header without the exact bytes Typeform actually sends.
This guide wires Typeform straight to your laptop with Relayers. You get a stable public URL, JQ-based routing, payload transformation, and a tunnel to localhost—all while the original request body and headers arrive untouched, so signature verification keeps working.
What you'll build
Typeform only ever sees a single, permanent Relayers URL. Relayers relays each delivery down a tunnel to whatever port your app is listening on.
Prerequisites
- A Typeform account with a published form and admin access to its Connect settings.
- A local webhook handler running on a port (this guide assumes
localhost:3000). - The Relayers app installed (download it here) and the
relayersCLI on your PATH. - A free Relayers account. The free tier covers everything below.
1Create an endpoint in Relayers
Open the Relayers app and go to Endpoints. Create a new endpoint—this gives you a public ingest URL shaped like:
https://api.relayers.app/v1/webhooks/wep_8f3c2a1b9d
Copy that URL with the Copy button. Before touching Typeform, hit Send test from the Endpoints view to confirm the endpoint is live and shows up in your Events list. This is the URL Typeform will call, and it never changes even as you reconfigure routing behind it.
2Add the webhook in Typeform
You can register the webhook two ways.
Through the UI: open your form, go to the Connect tab, choose Webhooks, and click Add a webhook. Paste your Relayers URL. If you want signed deliveries, set a secret—Typeform will use it to sign every request.
Through the Create Webhook API: if you automate form setup, register it programmatically:
curl -X PUT \
"https://api.typeform.com/forms/<form_id>/webhooks/relayers-dev" \
-H "Authorization: Bearer $TYPEFORM_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.relayers.app/v1/webhooks/wep_8f3c2a1b9d",
"enabled": true,
"secret": "your-signing-secret",
"verify_ssl": true
}'
When a secret is set, Typeform signs the payload and sends a Typeform-Signature header formatted as sha256=<base64 HMAC-SHA256 of the raw body>. Keep the secret handy—you'll verify against it locally in step 5's signature note.
3Forward events to localhost
Now bridge the endpoint to your machine. In a terminal:
relayers login
relayers listen --forward localhost:3000
relayers listen opens a WebSocket tunnel and streams matching deliveries to localhost:3000. Submit a test response to your Typeform form and watch it land on your handler within a second or two. You can also do this entirely from the desktop app: in Rules, set the destination to a tunnel pointing at localhost:3000.
Every request Relayers forwards carries the original body and headers untouched, so your handler sees exactly what Typeform sent—Typeform-Signature included.
4Route and filter with JQ
If several forms point at the same endpoint, or you only care about one form during development, add a Rule with a JQ filter. Rules are evaluated top to bottom and first match wins, so order them from most specific to least. You can reorder them by dragging.
To accept only responses from a single form:
{ "form_response": { "form_id": "abc123XY" } }
Anything that doesn't match this shape is skipped by the rule. Point the matching rule's destination at your localhost:3000 tunnel and leave broader forms to a lower-priority rule (or drop them entirely while you develop).
5Transform the payload
Typeform's form_response.answers[] array is verbose: each answer is an object with a field descriptor and a typed value (text, email, choice, and so on). For local testing you usually want a flat object keyed by your field refs.
In Transformations, create a jq transform and use the test playground to iterate against a real captured payload:
{
form_id: .form_response.form_id,
submitted_at: .form_response.submitted_at,
answers: (
.form_response.answers
| map({
field: .field.ref,
value: (.text // .email // .choice.label)
})
)
}
The // operator falls through answer types—.text for short/long text, .email for email fields, .choice.label for single choice—so each answer collapses to a simple { field, value } pair. Relayers supports jq, jsonata, javascript, and go_template; pick whichever you're fastest in. The playground shows the output before you save, so you never ship a broken mapping.
A note on signatures
Because Relayers forwards the raw request bytes, you can verify the Typeform-Signature header on localhost exactly as you would in production. Recompute base64(HMAC-SHA256(secret, raw_body)), prefix it with sha256=, and compare against the header. Verify against the raw body, not a re-serialized JSON object—reparsing changes the bytes and breaks the HMAC. Transformations run after delivery routing, so if you need the untransformed body for verification, point your tunnel at the raw endpoint and transform downstream.
Going to production
When your handler is ready to deploy, you don't touch Typeform again. Just change the Rule's destination from the localhost:3000 tunnel to your public URL (for example https://api.yourapp.com/hooks/typeform). The Relayers endpoint URL Typeform calls stays identical. You can export your endpoint, rules, and transformations as a JSON bundle and import them into another workspace or commit them alongside your code, so staging and production stay in sync.
Troubleshooting
- No deliveries arriving? Check Typeform's own delivery view under Connect → Webhooks—it logs each attempt and the response status it got back from Relayers. Then cross-check the Relayers Events view and use the payload inspector to see exactly what came in.
- Signature never validates? Confirm the secret in Typeform matches the one your code uses, and that you're hashing the raw body. A single whitespace difference from reparsing will fail the check.
- Answers look empty after transform? Your field
refvalues may differ from fieldids. Inspect a realform_response.answers[]entry in the payload inspector and confirm you're reading.field.ref(not.field.id) and the correct value key for that answer type. - Rule not matching? Remember first match wins. A broad earlier rule can swallow the delivery before your specific
form_idrule runs—reorder so the specific rule sits on top.
Wrap-up
With Relayers in the middle, Typeform webhooks reach your laptop over a stable URL, you filter to just the form you care about with JQ, flatten the answers into something usable, and keep signature verification intact the whole way through. When you're done developing, flip one destination and you're in production—no Typeform reconfiguration required.
Ready to test Typeform webhooks locally? Get started with Relayers and download the app—the free tier has everything you need.