Building a Messenger or Instagram bot means fighting two problems before you ever read a single message. First, Meta insists on a verification handshake: a GET request carrying hub.mode, hub.verify_token, and hub.challenge that your callback URL must echo back correctly, or the subscription silently refuses to save. Second, once you clear that gate, every inbound message arrives as a POST from Meta's servers — which cannot reach the API running on your laptop.
The usual workaround is to deploy to staging, tail remote logs, and message your own bot from a phone while squinting at a dashboard. That loop is slow and it hides the actual payload from your debugger. Relayers gives you a public HTTPS endpoint that forwards Meta's requests — untouched — straight to localhost, so both the handshake and real bot messages hit your breakpoints.
What you'll build
Prerequisites
- A Relayers account (the free tier covers this) and the desktop app installed.
- A Meta app with the Messenger and/or Instagram product added, and admin access to the Meta App Dashboard.
- A Facebook Page (for Messenger) or an Instagram professional account linked to that Page.
- Your bot running locally, listening on a port — we'll use
3000.
1Create an endpoint in Relayers
Open the desktop app, go to Endpoints, and create a new one. Give it a name like meta-messaging and copy the generated ingest URL:
https://api.relayers.app/v1/webhooks/wep_8fk29dl3
That wep_<id> URL is public, HTTPS, and stable — exactly what Meta's dashboard expects. Use Send test to fire a sample payload and confirm the endpoint is live before wiring anything else up.
2Configure the webhook in the Meta App Dashboard
In the Meta App Dashboard, open your app and go to Messenger → Webhooks (or Instagram → Configuration for IG messaging). Under Callback URL paste your Relayers ingest URL, and under Verify Token enter any secret string you choose — it just has to match what your handler compares against.
When you click Verify and Save, Meta sends a GET request to your callback URL:
GET /v1/webhooks/wep_8fk29dl3?hub.mode=subscribe
&hub.verify_token=your-secret-token
&hub.challenge=1158201444
Your local handler must confirm the token and echo the raw hub.challenge value back with a 200:
app.get("/webhook", (req, res) => {
const mode = req.query["hub.mode"];
const token = req.query["hub.verify_token"];
const challenge = req.query["hub.challenge"];
if (mode === "subscribe" && token === process.env.VERIFY_TOKEN) {
return res.status(200).send(challenge);
}
res.sendStatus(403);
});
Because Relayers forwards the query string and method verbatim, this handshake succeeds against localhost just as it would in production. Once verified, subscribe to the fields you need — messages and messaging_postbacks are the essentials — and, for Messenger, subscribe the specific Page to your app. For Instagram, make sure the IG account is subscribed under the Instagram product.
3Forward events to localhost
Point the tunnel at your bot. From a terminal:
relayers login
relayers listen --forward localhost:3000
Alternatively, do it in the desktop app: open Rules on your endpoint and set the destination to a tunnel → localhost:3000. Either way, every request Meta sends to wep_8fk29dl3 now replays against your local server. Re-run the verification in the dashboard and watch the GET land in your logs.
4Route and filter with JQ
Meta sends more than messages — it also delivers read receipts, delivery confirmations, and echoes of your own outbound messages. You usually want only genuine inbound messages hitting your handler.
Open Rules, add a JQ filter, and keep the request only when it carries an inbound message body:
.entry[0].messaging[0].message != null
Rules match top to bottom and first match wins, so you can stack more specific rules above general ones — for example, route postbacks to one localhost port and messages to another — and drag to reorder them. Everything that fails the filter simply isn't forwarded.
5Transform the payload
Meta's envelope is deeply nested. If your handler only cares about who sent what, use a Transformation to flatten it before it reaches your code. In the Transformations view, pick jq and use the test playground with a captured event:
{
sender: .entry[0].messaging[0].sender.id,
text: .entry[0].messaging[0].message.text
}
The playground shows the output side by side with the input, so you can iterate without redeploying. Relayers also supports JSONata, JavaScript, and Go templates if you prefer a different transform language.
A note on signatures
Meta signs every POST with an X-Hub-Signature-256 header — an HMAC-SHA256 of the raw body keyed on your app secret. Relayers forwards the original body and headers untouched, so that signature stays valid end to end. Your local verification code works exactly as it will in production:
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.APP_SECRET)
.update(rawBody)
.digest("hex");
// timing-safe compare against req.headers["x-hub-signature-256"]
If you apply a transformation, verify the signature before the transform runs, since HMAC is computed over the original bytes.
Going to production
When your bot is ready to ship, keep the same Relayers endpoint and just repoint the rule's destination from the tunnel to your public production URL. Nothing in the Meta App Dashboard changes — the callback URL and verify token stay identical, so there's no re-verification dance on deploy. You can also Export your endpoint, rules, and transformations as a JSON bundle and Import them into another workspace to reproduce the exact setup.
Troubleshooting
- Verification fails ("The URL couldn't be validated"). Confirm your handler echoes the raw
hub.challengewith a200and that the verify token matches exactly. Check the Events view and payload inspector in Relayers to see the actualGETand the response your local server returned. - Handshake works but no messages arrive. You subscribed the app but not the Page (Messenger) or IG account (Instagram), or you never subscribed the
messagesfield. Re-check the subscriptions in the dashboard. - Meta reports timeouts or resends. Meta expects a
200within a few seconds. Acknowledge fast and process asynchronously; don't block the response on slow downstream work. - Seeing your own messages loop back. Those are
message_echoes. Tighten the JQ filter —.entry[0].messaging[0].message.is_echo != true— to drop them.
Every request Meta sends is captured in Analytics and Events, so if something goes wrong you can inspect the exact payload and retry it against localhost without messaging your bot again.
Wrap-up
The Messenger and Instagram webhook lifecycle — verification handshake, field subscriptions, and signed message delivery — is entirely testable on your machine when the public URL forwards everything untouched. Create an endpoint, run relayers listen --forward localhost:3000, filter to inbound messages with JQ, and debug real bot conversations locally with signatures intact.
Get started at relayers.app and download the app to build your first Meta messaging endpoint in minutes.