← All guides
ChatwootJuly 12, 20266 min read

Test Chatwoot Webhooks on Localhost with Relayers

Receive, route, and transform a Chatwoot webhook on localhost. Build support automations locally with a JQ filter, jq transform, and a tunnel to your dev server.

You're building a support bot. Every time a customer sends a message in Chatwoot, you want your local service to react — auto-reply, enrich the conversation, ping an internal channel, run a classifier. But Chatwoot lives in the cloud, your code runs on localhost:3000, and there's no reachable URL in between. So you deploy to a staging box, tail the logs, tweak, redeploy, and repeat. Every iteration costs minutes you don't have.

Relayers closes that gap. It gives you a public ingest URL, tunnels the traffic straight to your laptop, and lets you filter and reshape the payload before it ever hits your handler — so you can iterate against real Chatwoot events without leaving your editor.

What you'll build

Chatwoot POSTs every subscribed event to a Relayers endpoint. Relayers filters out the noise (outgoing and bot messages), transforms the JSON into the shape your handler wants, and forwards it down a WebSocket tunnel to your local dev server.

Prerequisites

1Create an endpoint in Relayers

In the desktop app, open Endpoints and create a new one — call it chatwoot-dev. Copy the generated ingest URL. It looks like:

https://api.relayers.app/v1/webhooks/wep_9f2c1a7b

That wep_<id> is the public identifier for this endpoint. Anything Chatwoot POSTs here becomes an event you can inspect, route, and forward. Before wiring up Chatwoot, hit Send test to confirm the endpoint is live and events show up in the Events list.

2Add the webhook in Chatwoot

In Chatwoot, go to Account → Settings → Integrations → Webhooks → Add new webhook. Paste the Relayers ingest URL, then choose the events you care about. For a support bot, subscribe to:

Save it. Chatwoot will now POST a JSON body to your endpoint for every one of those events. A message_created payload looks roughly like this:

{
  "event": "message_created",
  "message_type": "incoming",
  "content": "Hey, my invoice didn't arrive",
  "conversation": { "id": 42 },
  "sender": { "id": 7, "name": "Ada" }
}

The event field tells you what happened; message_type distinguishes an inbound customer message (incoming) from your agent's reply or a bot message (outgoing).

3Forward events to localhost

Log in and start the tunnel from your terminal:

relayers login
relayers listen --forward localhost:3000

Now trigger a real event — send a message in a Chatwoot conversation. It hits the endpoint, travels down the tunnel, and lands on localhost:3000 in real time. Open the payload inspector in the desktop app to see the exact body and headers as Chatwoot sent them. Relayers forwards the original body and headers untouched, so what your handler receives is byte-for-byte what Chatwoot produced.

4Route and filter with JQ

Chatwoot is chatty. Every agent reply and bot message also fires message_created, and if your bot posts replies back into Chatwoot, those trigger events too — an easy way to build an infinite loop. Filter early.

Open Rules on the endpoint and add a JQ filter that only matches inbound customer messages:

{ "event": "message_created", "message_type": "incoming" }

This is a match object: the event passes only when its event is message_created and message_type is incoming. Everything else — outgoing replies, bot messages, status changes — is skipped by this rule. Set the rule's destination to your tunnel → localhost:3000. Rules are first-match-wins and reorderable, so you can add more specific rules above this one later (say, routing conversation_created to a different local port) and drag them into priority order.

5Transform the payload

Your bot doesn't need Chatwoot's full envelope — it needs the conversation ID and the text. Open Transformations, pick the jq engine, and slim the payload down:

{
  event: .event,
  conversation: .conversation.id,
  text: .content
}

Given the sample above, your handler now receives:

{ "event": "message_created", "conversation": 42, "text": "Hey, my invoice didn't arrive" }

Use the built-in test playground to paste a real Chatwoot payload and confirm the output before you save. Relayers also supports jsonata, javascript, and go_template engines if you'd rather transform in a different language.

Verifying authenticity

Chatwoot does not sign its webhooks with an HMAC signature by default, so there's no signature header to verify. The practical mitigation is a shared secret: append a hard-to-guess token to the URL you register in Chatwoot (for example .../wep_9f2c1a7b?token=<secret>) or, on self-hosted Chatwoot, add a custom header. Because Relayers preserves headers and the full URL, your local handler can check that token and reject anything that doesn't carry it.

Going to production

When your bot is ready to leave localhost, keep the same endpoint and just change the rule's destination from the tunnel to your deployed service's public URL — no change in Chatwoot, no new webhook URL. Your filter and transform keep working exactly as they did in dev.

To move the whole setup between environments or share it with a teammate, use Import/Export to save the endpoint, rules, and transformations as a JSON bundle and re-import it wherever you need it.

Troubleshooting

Wrap-up

With a Relayers endpoint, a JQ filter, and a tunnel, you can build and debug Chatwoot support automations entirely on localhost — against real events, with the loops filtered out and the payload already in the shape you want. When you're done, flip the destination to production and ship.

Get started at relayers.app — create an endpoint and download the app to forward your first Chatwoot event in minutes.