Updated · Works with n8n, Make, Slack · Coolie event: lead.replied
What the workflow does
Coolie webhook (Webhook)
Receives Coolie's POST, answers at once, and keeps the raw body, which the signature is computed on.
Verify Coolie signature (Code)
Recomputes the
Coolie-Signatureheader, an HMAC-SHA256 of the timestamp, a dot and the raw body with your webhook's secret, compares it in constant time, refuses a request older than five minutes, and passes the event on.Replies only (IF)
Lets
lead.repliedevents through, and Coolie's test event while you set it up.Post to Slack (Slack)
Posts who answered and at which company, the channel, the subject and the text, or that they asked to stop.
Set it up in n8n
Import the workflow
Download the JSON file below. In n8n, open a new workflow and choose Import from File, or Import from URL with https://trycoolie.ai/templates/n8n/coolie-lead-replied-to-slack.json, or paste the JSON onto the canvas.
Connect Slack
Open Post to Slack, create or pick a Slack credential, and choose the channel.
Copy the webhook's address
Open Coolie webhook and copy its Production URL: the address Coolie will post to.
Add the webhook in Coolie
In Coolie, open Settings → Webhooks, paste the address, tick Lead replied (
lead.replied) and click Add webhook: nothing is sent yet. Then click Reveal and copy the webhook's secret, which starts withwhsec_.Paste the secret
In n8n, open Verify Coolie signature and paste the secret between the quotes on its first lines. Without it, the node stops every event, with an error that says so.
Publish the workflow
Save, then publish the workflow (on n8n before version 2.0, switch it to active): its Production URL now answers.
Send a test
Back in Coolie, click Send test on the webhook: a test message appears in your channel.
coolie-lead-replied-to-slack.json
An n8n workflow (Webhook, Code, IF and the tools' own nodes), with a note on the canvas that repeats these steps.
Show the workflow JSON
{
"name": "Coolie: lead replied → Slack",
"nodes": [
{
"parameters": {
"content": "## Coolie: alert Slack when a lead replies\n\nWhen a lead answers one of your Coolie campaigns, by email or on LinkedIn, or asks it to stop, this workflow posts the reply to a Slack channel.\n\n### How it works\n1. **Coolie webhook** receives the **lead.replied** event, raw body kept.\n2. **Verify Coolie signature** checks the `Coolie-Signature` header with your webhook's secret and stops anything else.\n3. **Replies only** lets `lead.replied` events through, and Coolie's test event.\n4. **Post to Slack** sends who answered, at which company, on which channel, and their words.\n\n### Setup\n1. In **Post to Slack**, connect Slack and pick the channel.\n2. Copy the **Production URL** of **Coolie webhook**.\n3. In Coolie: **Settings → Webhooks**, paste the URL, tick **Lead replied**, click **Add webhook**, then **Reveal** the secret.\n4. Paste the secret at the top of **Verify Coolie signature**.\n5. Save and publish the workflow (on n8n before 2.0, activate it).\n6. Click **Send test** in Coolie: a test message appears in Slack.\n\nOut-of-office answers and bounces are not replies: they never reach this workflow.\nSelf-hosted n8n: allow the crypto module (`NODE_FUNCTION_ALLOW_BUILTIN=crypto`, on the task runners when you use them).\nGuide: https://trycoolie.ai/developers/templates/lead-replied-to-slack",
"height": 700,
"width": 480
},
"id": "a341ba3b-9d99-4bb6-889d-a04cbbb4d8cc",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-560,
-280
]
},
{
"parameters": {
"httpMethod": "POST",
"path": "coolie-lead-replied",
"responseMode": "onReceived",
"options": {
"rawBody": true
}
},
"id": "72b85a0f-2c28-4f24-8d2d-c8510ad6f032",
"name": "Coolie webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
0
],
"webhookId": "38deca36-b278-432a-8620-1100417e64f3"
},
{
"parameters": {
"jsCode": "// Coolie signs every event: Coolie-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of \"t.body\">.\n// This node recomputes it on the raw body with your webhook's secret, and stops anything else.\n// Paste the secret below: Coolie > Settings > Webhooks > Reveal (it starts with whsec_).\nconst SECRET = 'PASTE_YOUR_COOLIE_WEBHOOK_SECRET';\n\nconst crypto = require('crypto');\nif (!SECRET.startsWith('whsec_')) {\n throw new Error('Paste your Coolie webhook secret (whsec_...) in the Verify Coolie signature node.');\n}\nconst request = $input.first().json;\nconst rawBody = (await this.helpers.getBinaryDataBuffer(0, 'data')).toString('utf8');\nconst parts = Object.fromEntries(\n String(request.headers['coolie-signature'] || '').split(',').map((part) => part.split('=')),\n);\nconst expected = Buffer.from(crypto.createHmac('sha256', SECRET).update(parts.t + '.' + rawBody).digest('hex'));\nconst given = Buffer.from(String(parts.v1 || ''));\nconst fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;\nif (!fresh || given.length !== expected.length || !crypto.timingSafeEqual(given, expected)) {\n throw new Error('Not a Coolie event: the signature does not match.');\n}\nreturn [{ json: JSON.parse(rawBody), pairedItem: { item: 0 } }];"
},
"id": "b44bcfef-1a74-479a-86fa-e21cd1b52de7",
"name": "Verify Coolie signature",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
240,
0
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "551a68cb-2f6d-42dc-8e2c-2885b45f56ea",
"leftValue": "={{ $json.type }}",
"rightValue": "lead.replied",
"operator": {
"type": "string",
"operation": "equals"
}
},
{
"id": "fe1d48a2-ce4b-4707-85db-802e5702a238",
"leftValue": "={{ $json.type }}",
"rightValue": "test",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "or"
},
"options": {}
},
"id": "ef225f27-2093-443f-8d85-70c26706b09a",
"name": "Replies only",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
480,
0
]
},
{
"parameters": {
"select": "channel",
"channelId": {
"__rl": true,
"mode": "list",
"value": ""
},
"text": "=:speech_balloon: *{{ $json.type === 'test' ? 'Test event from Coolie' : ($json.data.reply.kind === 'stop' ? 'A lead asked to stop' : 'A lead replied by ' + $json.data.reply.channel) }}*\n{{ $json.data.lead && $json.data.lead.contact ? $json.data.lead.contact.name + ($json.data.lead.contact.title ? ', ' + $json.data.lead.contact.title : '') : 'Lead' }}{{ $json.data.lead ? ' at ' + $json.data.lead.company.name : '' }}\n{{ $json.data.reply && $json.data.reply.subject ? '*' + $json.data.reply.subject + '*' : '' }}\n{{ $json.data.reply ? ($json.data.reply.text || '(no text)') : $json.data.message }}",
"otherOptions": {
"includeLinkToWorkflow": false
}
},
"id": "d68d1525-f16c-4ab4-8765-28e7e0303d5c",
"name": "Post to Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.2,
"position": [
720,
0
]
}
],
"connections": {
"Coolie webhook": {
"main": [
[
{
"node": "Verify Coolie signature",
"type": "main",
"index": 0
}
]
]
},
"Verify Coolie signature": {
"main": [
[
{
"node": "Replies only",
"type": "main",
"index": 0
}
]
]
},
"Replies only": {
"main": [
[
{
"node": "Post to Slack",
"type": "main",
"index": 0
}
],
[]
]
}
},
"pinData": {},
"settings": {
"executionOrder": "v1"
}
}Build it in Make
Make's scenario files are its own, so here is the same flow, module by module, in a new scenario.
Create the webhook in Make
In a new scenario, add Webhooks › Custom webhook, click Add, name it (Coolie leads) and copy its address. Make then waits for a first request to learn the data's structure.
Send Coolie's test event
In Coolie, open Settings → Webhooks, paste the address, tick Lead replied (
lead.replied), click Add webhook, then Send test. Make shows that it determined the structure from the test event, which carries a sample lead in the real shape.Teach Make the reply's fields
The test event carries a lead but no reply. To map the reply before the first real one, give the webhook a data structure generated from the sample below, or redetermine the structure when a first reply arrives.
Add the Slack message
Add Slack › Create a Message, choose the channel, and write the text from data › lead › contact › name, data › lead › company › name, and data › reply › kind, channel, subject and text.
Filter on replies
On the link between the two modules, set up a filter: type Equal to lead.replied.
Turn the scenario on
Save, and switch the scenario on, set to run immediately as data arrives.
Optional: check Coolie's signature
A Make webhook address is long and random: keep it private. To refuse anything Coolie did not sign, turn on Get request headers and JSON pass-through in the webhook's advanced settings, parse the text it passes with JSON › Parse JSON, and add a filter comparing the
v1part of theCoolie-Signatureheader with Make'ssha256function given your secret as its key (it then computes an HMAC, in hex), over thetpart of the header, a dot and the raw body.
A lead.replied event
The lead, as GET /api/v1/leads gives it (null if it was deleted since), and the reply: kind reply or stop, channel email or linkedin, its subject, its text and when it came in. The values are examples.
{
"id": "evt_Lm5pW0qZ8rTc3XyA",
"type": "lead.replied",
"createdAt": "2026-10-21T14:07:31.502Z",
"data": {
"lead": {
"runId": "ui-5b9e2c41",
"prospectId": "p-41b7c2d9e0aa",
"agentId": "agent-3f2a91c0",
"status": "written",
"processedAt": "2026-10-19T09:02:13.511Z",
"icpMatch": true,
"aiScore": 8.4,
"feedback": null,
"signal": {
"type": "hiring",
"name": "Hiring",
"headline": "Operations Manager",
"detail": null,
"url": "https://jobs.example.com/12345",
"postedAt": "2026-10-17"
},
"company": {
"name": "Northwind Health",
"domain": "northwind.example",
"website": "https://northwind.example",
"linkedinUrl": "https://www.linkedin.com/company/example",
"industry": "Hospitals and Health Care",
"employees": 320,
"country": "France",
"locations": [
"Lyon, France"
]
},
"contact": {
"name": "Camille Martin",
"title": "COO",
"seniority": "c_suite",
"linkedinUrl": "https://www.linkedin.com/in/example",
"country": "France",
"email": "camille.martin@northwind.example",
"emailConfidence": 0.92,
"phone": null
},
"research": null,
"sequence": null
},
"reply": {
"campaignId": "cmp-4e1a9c7b2d80",
"queueItemId": "q-8b2f6d0e9a13",
"kind": "reply",
"channel": "email",
"subject": "Re: Two clinics, one schedule",
"text": "Hi, good timing. Could we talk on Thursday afternoon?",
"receivedAt": "2026-10-21T14:05:12.000Z"
}
}
}Frequently asked questions
Which answers trigger the alert?
A reply to one of your campaign's emails or LinkedIn messages, and a request to stop (kind stop). Out-of-office answers and bounces do not.
Does the campaign stop when the lead replies?
When its sending rules pause on reply, the lead's next steps stop. This alert only tells your team; Coolie's inbox holds the whole conversation.
Is the lead always in the event?
Yes, unless it was deleted since: data.lead is then null, and the reply is still told.
Does Coolie have a Slack app?
No. This template connects the two through Coolie's signed webhooks: Coolie posts each event to n8n or Make, which posts to Slack. The same events can feed any tool n8n or Make reaches.
My self-hosted n8n refuses require('crypto'). What do I do?
The Code node that checks the signature needs Node's crypto module. n8n Cloud allows it; on a self-hosted n8n, set NODE_FUNCTION_ALLOW_BUILTIN=crypto (on the task runners when you use them) and restart.
How the events are signed and retried: receive Coolie webhooks and verify their signature. Every template: Coolie workflow templates.
