$npx -y skills add hookdeck/webhook-skills --skill intercom-webhooksReceive and verify Intercom webhooks. Use when setting up Intercom webhook handlers, debugging X-Hub-Signature verification, or handling customer messaging events like conversation.user.created, conversation.admin.replied, contact.user.created, or ticket.created.
| 1 | # Intercom Webhooks |
| 2 | |
| 3 | ## When to Use This Skill |
| 4 | |
| 5 | - Setting up Intercom webhook handlers (Developer Hub topic subscriptions) |
| 6 | - Debugging `X-Hub-Signature` (HMAC-SHA1) verification failures |
| 7 | - Handling conversation, contact, and ticket events |
| 8 | - Responding to the `ping` handshake when registering a webhook |
| 9 | |
| 10 | ## Essential Code (USE THIS) |
| 11 | |
| 12 | Intercom signs every webhook with HMAC-SHA1 over the **raw JSON body** using your |
| 13 | app's `client_secret` (from the Developer Hub → Basic Info page). The signature is |
| 14 | sent in the `X-Hub-Signature` header as `sha1=<hex_digest>` (40 hex chars). |
| 15 | |
| 16 | ### Intercom Signature Verification (JavaScript) |
| 17 | |
| 18 | ```javascript |
| 19 | const crypto = require('crypto'); |
| 20 | |
| 21 | function verifyIntercomWebhook(rawBody, signatureHeader, clientSecret) { |
| 22 | if (!signatureHeader || !clientSecret) return false; |
| 23 | |
| 24 | // Intercom sends: sha1=<hex> |
| 25 | const [algorithm, signature] = signatureHeader.split('='); |
| 26 | if (algorithm !== 'sha1' || !signature) return false; |
| 27 | |
| 28 | const expected = crypto |
| 29 | .createHmac('sha1', clientSecret) |
| 30 | .update(rawBody) |
| 31 | .digest('hex'); |
| 32 | |
| 33 | try { |
| 34 | return crypto.timingSafeEqual( |
| 35 | Buffer.from(signature, 'hex'), |
| 36 | Buffer.from(expected, 'hex') |
| 37 | ); |
| 38 | } catch { |
| 39 | return false; |
| 40 | } |
| 41 | } |
| 42 | ``` |
| 43 | |
| 44 | ### Express Webhook Handler |
| 45 | |
| 46 | ```javascript |
| 47 | const express = require('express'); |
| 48 | const app = express(); |
| 49 | |
| 50 | // CRITICAL: Use express.raw() — Intercom signs the raw body, not parsed JSON |
| 51 | app.post('/webhooks/intercom', |
| 52 | express.raw({ type: 'application/json' }), |
| 53 | (req, res) => { |
| 54 | const signature = req.headers['x-hub-signature']; |
| 55 | |
| 56 | // Verify signature |
| 57 | if (!verifyIntercomWebhook(req.body, signature, process.env.INTERCOM_CLIENT_SECRET)) { |
| 58 | console.error('Intercom signature verification failed'); |
| 59 | return res.status(401).send('Invalid signature'); |
| 60 | } |
| 61 | |
| 62 | // Parse the payload after verification |
| 63 | const notification = JSON.parse(req.body.toString()); |
| 64 | const topic = notification.topic; |
| 65 | |
| 66 | console.log(`Received ${topic} (notification id: ${notification.id})`); |
| 67 | |
| 68 | // Handle by topic |
| 69 | switch (topic) { |
| 70 | case 'ping': |
| 71 | // Handshake when you save the webhook in the Developer Hub |
| 72 | console.log('Ping received'); |
| 73 | break; |
| 74 | case 'conversation.user.created': |
| 75 | console.log('New conversation from user:', notification.data.item.id); |
| 76 | break; |
| 77 | case 'conversation.user.replied': |
| 78 | console.log('User replied:', notification.data.item.id); |
| 79 | break; |
| 80 | case 'conversation.admin.replied': |
| 81 | console.log('Admin replied:', notification.data.item.id); |
| 82 | break; |
| 83 | case 'conversation.admin.assigned': |
| 84 | console.log('Conversation assigned:', notification.data.item.id); |
| 85 | break; |
| 86 | case 'contact.user.created': |
| 87 | console.log('New user:', notification.data.item.id); |
| 88 | break; |
| 89 | case 'contact.lead.created': |
| 90 | console.log('New lead:', notification.data.item.id); |
| 91 | break; |
| 92 | case 'ticket.created': |
| 93 | console.log('New ticket:', notification.data.item.id); |
| 94 | break; |
| 95 | default: |
| 96 | console.log('Unhandled topic:', topic); |
| 97 | } |
| 98 | |
| 99 | res.status(200).send('OK'); |
| 100 | } |
| 101 | ); |
| 102 | ``` |
| 103 | |
| 104 | ### Python Signature Verification (FastAPI) |
| 105 | |
| 106 | ```python |
| 107 | import hmac |
| 108 | import hashlib |
| 109 | |
| 110 | def verify_intercom_webhook(raw_body: bytes, signature_header: str, client_secret: str) -> bool: |
| 111 | if not signature_header or not client_secret: |
| 112 | return False |
| 113 | |
| 114 | # Intercom sends: sha1=<hex> |
| 115 | try: |
| 116 | algorithm, signature = signature_header.split("=", 1) |
| 117 | except ValueError: |
| 118 | return False |
| 119 | if algorithm != "sha1" or not signature: |
| 120 | return False |
| 121 | |
| 122 | expected = hmac.new( |
| 123 | client_secret.encode("utf-8"), |
| 124 | raw_body, |
| 125 | hashlib.sha1, |
| 126 | ).hexdigest() |
| 127 | return hmac.compare_digest(signature, expected) |
| 128 | ``` |
| 129 | |
| 130 | > **For complete working examples with tests**, see: |
| 131 | > - [examples/express/](examples/express/) - Full Express implementation |
| 132 | > - [examples/nextjs/](examples/nextjs/) - Next.js App Router implementation |
| 133 | > - [examples/fastapi/](examples/fastapi/) - Python FastAPI implementation |
| 134 | |
| 135 | ## Common Topics (Event Types) |
| 136 | |
| 137 | | Topic | Description | |
| 138 | |-------|-------------| |
| 139 | | `ping` | Handshake sent when the webhook is created/saved | |
| 140 | | `conversation.user.created` | New conversation started by a user | |
| 141 | | `conversation.user.replied` | User replied to a conversation | |
| 142 | | `conversation.admin.replied` | Admin (teammate) replied to a conversation | |
| 143 | | `conversation.admin.assigned` | Conversation assigned to an admin | |
| 144 | | `conversation.admin.closed` | Admin closed a conversation | |
| 145 | | `co |