# Guide to Intercom Webhooks: Features and Best Practices

Intercom webhooks notify your application when conversations, contacts, and tickets change. If you're routing support events, syncing contacts to a CRM, or mirroring tickets, webhooks are how you react without polling.

This guide covers how Intercom webhooks work, the topics you'll subscribe to, how to verify the `X-Hub-Signature` (it's SHA-1), and the ping handshake.

## What are Intercom webhooks?

Intercom webhooks are JSON POSTs delivered to a URL you configure in the Developer Hub. Each is signed with an `X-Hub-Signature` header formatted `sha1=<hex>`. The signature is an HMAC-SHA1 (a legacy scheme, not the modern SHA-256) over the raw request body, keyed with your app's client_secret, not a separate webhook secret and not the access token.

## Intercom webhook features

| Feature | Details |
| --- | --- |
| Configuration | Developer Hub > your app > Webhooks; subscribe to topics |
| Signature header | `X-Hub-Signature`, formatted `sha1=<hex>` |
| Signature scheme | HMAC-SHA1 (hex) over the raw body, keyed with the app `client_secret` |
| Setup handshake | A signed `ping` on save; verify it and return 2xx |
| Idempotency | Dedupe on `notification.id` |
| SDK | None for verification; verify manually |

## Common events

Intercom calls its events topics. A representative set:

| Topic | Fires when |
| --- | --- |
| `ping` | Intercom validates the endpoint on save |
| `conversation.user.created` / `conversation.user.replied` | A user starts or replies to a conversation |
| `conversation.admin.replied` / `.assigned` / `.closed` / `.noted` | An admin acts on a conversation |
| `contact.user.created` / `contact.lead.created` | A contact or lead is created |
| `contact.user.tag.created` | A contact is tagged |
| `ticket.created` / `ticket.admin.assigned` / `ticket.state.updated` | A ticket changes |

This isn't exhaustive; subscribe to the topics your use case needs in the Developer Hub.

> See Intercom webhook payloads in action. Inspect and replay sample Intercom webhook payloads in the [Hookdeck Console](https://console.hookdeck.com) — no account or setup required.

## Setting up Intercom webhooks

In the Developer Hub, open your app > Webhooks, set the endpoint URL, choose the API version, and select topics. Get the signing key from Basic Information > Client secret and store it as `INTERCOM_CLIENT_SECRET`. Rotating the client secret invalidates signatures for all webhooks, so rotate and update the env in lockstep.

## Securing Intercom webhooks

Split the `X-Hub-Signature` on `=`, confirm the algorithm is `sha1`, compute an HMAC-SHA1 over the raw body with your `client_secret`, hex-encode it, and compare in constant time. Verify against the raw body before parsing.

```javascript
const crypto = require("crypto");

const CLIENT_SECRET = process.env.INTERCOM_CLIENT_SECRET;

function verify(rawBody, signatureHeader) {
  if (!signatureHeader) return false;
  const [algorithm, signature] = signatureHeader.split("=");
  if (algorithm !== "sha1" || !signature) return false;

  const expected = crypto.createHmac("sha1", CLIENT_SECRET).update(rawBody).digest("hex");
  try {
    return crypto.timingSafeEqual(Buffer.from(signature, "hex"), Buffer.from(expected, "hex"));
  } catch {
    return false;
  }
}

app.post("/webhooks/intercom", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body, req.headers["x-hub-signature"])) {
    return res.sendStatus(401);
  }

  const payload = JSON.parse(req.body.toString());
  if (payload.topic === "ping") return res.sendStatus(200); // handshake

  res.sendStatus(200); // acknowledge fast
  processQueue.add(payload); // dedupe on notification.id, async
});

```

The same check in Python:

```python
import hashlib
import hmac
import os

CLIENT_SECRET = os.environ["INTERCOM_CLIENT_SECRET"].encode()

def verify(raw_body: bytes, signature_header: str) -> bool:
    if not signature_header:
        return False
    try:
        algorithm, signature = signature_header.split("=", 1)
    except ValueError:
        return False
    if algorithm != "sha1" or not signature:
        return False
    expected = hmac.new(CLIENT_SECRET, raw_body, hashlib.sha1).hexdigest()
    return hmac.compare_digest(signature, expected)

```

> Make Intercom webhooks production-ready. [Hookdeck Event Gateway](/event-gateway) verifies the `X-Hub-Signature`, deduplicates, and durably queues every event.

## Intercom webhook limitations and pain points

### It's SHA-1, not SHA-256

The Problem: `X-Hub-Signature` is HMAC-SHA1, the legacy scheme (not GitHub's modern `X-Hub-Signature-256`). Using SHA-256 never matches.

Why It Happens: Intercom uses the older SHA-1 `X-Hub-Signature`.

Workarounds:

* Use SHA-1, hex, over the raw body, and strip the `sha1=` prefix.

How Hookdeck Can Help: Hookdeck verifies the signature at the edge with the right algorithm, so your app doesn't hard-code SHA-1.

### The key is the app client_secret

The Problem: There's no separate webhook signing secret. Signatures are keyed with the app's `client_secret`, and using the access token (a common mix-up) fails.

Why It Happens: Intercom reuses the app client secret for webhook signing.

Workarounds:

* Key the HMAC with `client_secret` from Basic Information, not the access token.

How Hookdeck Can Help: Hookdeck holds the secret and verifies centrally, so the right key is used in one place.

### The ping handshake

The Problem: On save, Intercom sends a signed `ping`. A handler that tries to process it as a data event, or that skips verification for it, mishandles the setup check.

Why It Happens: Intercom validates the endpoint with a ping when you save the webhook.

Workarounds:

* Verify the ping like any delivery, then return 2xx without treating it as a data event.

How Hookdeck Can Help: Hookdeck can accept the ping and forward only real events to your handler.

### No timestamp, so dedupe on notification.id

The Problem: There's no timestamp or replay window, and retries can redeliver the same event.

Why It Happens: Intercom's scheme has no timestamp; delivery is at-least-once.

Workarounds:

* Dedupe on `notification.id` and make handlers idempotent.

How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Best practices

### Verify HMAC-SHA1 over the raw body with client_secret

Confirm the `sha1=` prefix, compute the hex HMAC-SHA1 over the raw body, and compare in constant time.

### Handle the ping

Verify the `ping` and return 2xx so the webhook saves, without processing it as data.

### Dedupe on notification.id

Retries can redeliver, so make handlers idempotent.

### Acknowledge fast, process asynchronously

Return 2xx quickly (Intercom expects a fast response) and defer work to a queue. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Intercom webhooks are verified with an `X-Hub-Signature` HMAC-SHA1 over the raw body, formatted `sha1=<hex>` and keyed with your app's `client_secret`. Handle the signed `ping` on save, verify over the raw body, dedupe on `notification.id`, and acknowledge fast.

[Hookdeck](https://hookdeck.com) verifies the signature, handles the ping, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique events.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Intercom webhooks reliably in minutes.