Test Intercom webhooks locally
Receive Intercom webhook notifications on localhost with a Horizon tunnel, answer the HEAD validation request, and verify X-Hub-Signature.
Receive Intercom notifications on your laptop while you build, with a URL Intercom can reach.
Before you begin
- Node.js 18 or later
- A Horizon account and the CLI (see Getting started)
- A reserved subdomain for
-s. Reserve one on the Subdomains page. - An Intercom app in the Developer Hub, with its client secret from the app's Basic Info page
Start your app
Intercom sends the HMAC in X-Hub-Signature as sha1=<40 hex characters>. It uses HMAC SHA1 over the JSON request body, with your app's client secret as the key. Read the body with request.text() before you parse it.
Intercom also sends a HEAD request to validate the URL, so the file exports a HEAD handler.
import { createHmac, timingSafeEqual } from "node:crypto";
export async function HEAD() {
return new Response(null, { status: 200 });
}
export async function POST(request: Request) {
const clientSecret = process.env.INTERCOM_CLIENT_SECRET;
if (!clientSecret) {
return new Response("Missing INTERCOM_CLIENT_SECRET", { status: 500 });
}
const body = await request.text();
const received = request.headers.get("x-hub-signature") ?? "";
const expected = `sha1=${createHmac("sha1", clientSecret).update(body).digest("hex")}`;
const receivedBuffer = Buffer.from(received);
const expectedBuffer = Buffer.from(expected);
const isValid =
receivedBuffer.length === expectedBuffer.length &&
timingSafeEqual(receivedBuffer, expectedBuffer);
if (!isValid) {
return new Response("Invalid signature", { status: 401 });
}
const notification = JSON.parse(body);
console.log(`Received Intercom topic: ${notification.topic}`);
return new Response("ok", { status: 200 });
}Store the client secret in an environment variable.
INTERCOM_CLIENT_SECRET=your-client-secretStart the app on port 3000.
npm run devIntercom gives you five seconds to respond. A late notification counts as failed and is retried once after one minute.
Start a tunnel
Use -s with a subdomain you reserved. Without it, the subdomain is random and changes every run, so your webhook would point at a dead URL after a restart. Reserve it first on the Subdomains page. Reserved subdomains are a paid feature, see Pricing.
hrzn tunnel http://localhost:3000 -s my-intercomHorizon prints HORIZON: Tunnel connected. Your public URL is https://my-intercom.hrzn.run. Keep this terminal open.
Add the webhook in Intercom
- Open your app in the Developer Hub.
- Under Configure, select Webhooks.
- Enter
https://my-intercom.hrzn.run/api/webhooks/intercomas the endpoint URL. It must be HTTPS. - Open the topics dropdown and select the topics you need, for example
contact.user.created. - Select Save.
Each topic needs a matching permission scope on your app's Authentication page. Removing the scope stops that topic's notifications.
Trigger a notification
Intercom's docs describe no resend or test button. Trigger a real event instead. With contact.user.created selected, create a user in your Intercom workspace. Pick the action that matches each topic you subscribe to.
Check it works
Create a user. The Horizon terminal prints one line:
POST 200 /api/webhooks/intercomYour app terminal prints:
Received Intercom topic: contact.user.createdTroubleshooting
The signature doesn't match
- Use the client secret from your app's Basic Info page, not an access token.
- Compute the HMAC over the raw body. Don't run
JSON.parseandJSON.stringifyfirst. - Use SHA1, and prefix the hex digest with
sha1=. - Restart
npm run devafter you edit.env.local.
Intercom can't validate the URL
Intercom checks the URL with a HEAD request. Check that the route exports HEAD, the tunnel is running, and the path is /api/webhooks/intercom.
No notifications arrive
Check that your app has the permission scope for the topic on the Authentication page.
The URL changed after a restart
You started the tunnel without -s, so Horizon gave you a new random subdomain. Restart with -s my-intercom and update the endpoint URL on the Webhooks page. -s needs a subdomain you reserved, see Pricing.
Next steps
- Read Intercom's webhook documentation.
Test Airship webhooks locally
Receive Airship Real-Time Data Streaming webhook events on localhost with a Horizon tunnel, and protect the endpoint with a custom header secret.
Test Mailchimp webhooks locally
Receive Mailchimp audience webhooks on localhost with a Horizon tunnel, answer the URL check, and verify the X-Mailchimp-Signature header.