Horizon

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.

app/api/webhooks/intercom/route.ts
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.

.env.local
INTERCOM_CLIENT_SECRET=your-client-secret

Start the app on port 3000.

npm run dev

Intercom 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-intercom

Horizon prints HORIZON: Tunnel connected. Your public URL is https://my-intercom.hrzn.run. Keep this terminal open.

Add the webhook in Intercom

  1. Open your app in the Developer Hub.
  2. Under Configure, select Webhooks.
  3. Enter https://my-intercom.hrzn.run/api/webhooks/intercom as the endpoint URL. It must be HTTPS.
  4. Open the topics dropdown and select the topics you need, for example contact.user.created.
  5. 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:

Output
  POST    200  /api/webhooks/intercom

Your app terminal prints:

Output
Received Intercom topic: contact.user.created

Troubleshooting

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.parse and JSON.stringify first.
  • Use SHA1, and prefix the hex digest with sha1=.
  • Restart npm run dev after 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

On this page