message.comDevelopers

Test webhooks locally with ngrok

Webhooks need an HTTPS endpoint, so testing on localhost is annoying. ngrok opens a public HTTPS tunnel to your dev machine in seconds, with a stable URL on paid plans. This tutorial walks the full loop for any message.com webhook you build against.

Cloudflare Tunnel (cloudflared), localtunnel, and Tailscale Funnel all work equivalently. We use ngrok in the examples because it is the most ubiquitous.

1. Install and start ngrok

bash
# Install ngrok then start a tunnel to your local server
ngrok http 3000

# ngrok prints a forwarding URL like:
# Forwarding   https://abc12.ngrok.app -> http://localhost:3000

Free accounts get a random subdomain each run. Paid accounts get a stable subdomain (worth it for ongoing dev).

2. Write a minimal handler

server.ts
// Minimal Express server that prints inbound webhook bodies
import express from "express";

const app = express();
app.use(express.json({ limit: "10mb" }));

app.post("/webhooks/message/email-inbound", (req, res) => {
  console.log("inbound:", JSON.stringify(req.body, null, 2));
  console.log("signature:", req.get("x-signature"));
  res.status(200).end();
});

app.listen(3000, () => console.log("listening on 3000"));

Run it: node --watch server.ts (or your equivalent). Confirm it logs "listening on 3000".

3. Point a webhook at the tunnel

Depends on which event you are testing:

  • Outgoing webhooks from message.com. Configure under Settings → Integrations → Webhooks. Paste the ngrok URL plus the path your handler listens on.
  • Inbound email. Point the webhook URL at https://abc12.ngrok.app/webhooks/email/inbound.
  • Inbound calls. Same pattern; the URL goes in the calls webhook config.

4. Trigger an event

Two options:

  • Real event. Send a real email to your inbound address; let the ESP webhook fire.
  • Replay. ngrok's web dashboard (http://127.0.0.1:4040) lets you replay any past request. Useful for debugging signature failures.

5. Verify the signature

Every webhook from message.com is signed. The signature header and verification pattern depend on the source:

  • Email inbound. Custom X-Signature: sha256=<hex>. HMAC-SHA256 of raw body.
  • Email outbound. Svix headers (svix-id, svix-timestamp, svix-signature).
  • Calls inbound. Per-workspace HMAC against the secret stored on the workspace.
  • Outgoing webhooks (us → your URL). X-Message-Signature header. HMAC-SHA256 of raw body using your endpoint's shared secret.

Always verify on raw bytes before parsing JSON. See Signature verification.

6. Iterate

ngrok's inspector at http://127.0.0.1:4040 shows every request, headers, body, your response. Replay one request, change your code, see the new response. Faster than waiting for the source to fire again.

Common pitfalls

  • Free subdomain churns. Each ngrok http run gets a new URL. Update the webhook config or get a static subdomain.
  • JSON body parser eating raw bytes. Most signature schemes need the raw bytes. Set up a raw parser only on the webhook route, JSON parser elsewhere.
  • Body size limits. ngrok's free tier caps at 10MB per request. Large attachments need a paid plan or a different tunnel.
  • HTTPS only. Use the https:// URL, not the http:// URL. Webhook senders reject plain HTTP.

Next steps