Docs: Notifications and webhooks
Send notifications to where your team works
A project can tell a channel in Slack, Microsoft Teams, Discord or Google Chat when a client leaves a note, finishes a round or replies. It can also send each of those to a webhook of your own as signed JSON, for Zapier, Make, n8n or your own code.
Before you start
- Plan. Notifications to Slack, Microsoft Teams, Discord and Google Chat are on every plan. The outgoing webhook is part of the Agency and Scale plans. The free trial is the Agency plan, so you can try the webhook during the trial. On Starter a webhook you added earlier is kept, and is not sent to.
- Role. Destinations are managed by the workspace’s owner and its admins.
- Email is not changed. Your team is still emailed when a client finishes a round, and everyone in a note’s thread is still emailed about replies and mentions. Notifications are as well as those emails, not in place of them.
Add a destination
- Open the project, then Notifications. It is one of the project’s pages, listed in the sidebar, or across the top of the page on a phone.
- Under Add a destination, choose where it goes and paste the address that service gave you. The sections below say where to find it.
- Give it a name if you like, such as the channel’s, and tick what it should be told about: A new note, A round is finished, A reply.
- Press Add destination, then Send a test event. A message that says it is a test arrives in the channel, and the page says what the service answered.
A project can have 5 destinations, and each has its own ticks, so one channel can hear about every note while another hears only about finished rounds.
An address is a secret: anyone who has it can post to that channel. Once it is saved, ClickNote keeps it encrypted and never shows it again. The page shows only which service it is and the last four characters of the address, with the host’s name for a webhook of your own. A destination’s name and what it is told about are changed under Edit, in its row. To change an address, remove the destination there and add the new one.
Slack
- In Slack, create an app for your workspace, or open one you already have.
- In the app’s settings, choose Incoming Webhooks and switch on Activate Incoming Webhooks.
- Press Add New Webhook to Workspace, pick the channel and authorise it.
- Copy the address listed under Webhook URLs for Your Workspace. It starts
https://hooks.slack.com/services/.
Slack’s own guide: Sending messages using incoming webhooks.
Microsoft Teams
- In Teams, open More options (...) beside the channel and choose Workflows.
- Pick the template Send webhook alerts to a channel, choose the team and channel, and save it.
- Copy the webhook link the workflow gives you.
Use that template, and not the ones “from specific people” or “from people in an org”: those need a sign-in that ClickNote does not have. The workflow belongs to the person who made it, so make it from an account that will stay.
Older addresses do not work. Microsoft switched off Office 365 connectors, whose addresses are on webhook.office.com, in May 2026, and moved Workflows links off logic.azure.com at the end of November 2025. If you paste one of those, the page says so. A current link is on a host ending environment.api.powerplatform.com.
Microsoft’s own guide: Send messages in Teams using incoming webhooks.
Discord
- In Discord, open Server Settings and go to Integrations.
- Press Create Webhook and choose the channel it posts to.
- Press Copy Webhook URL. The address starts
https://discord.com/api/webhooks/.
Discord’s own guide: Intro to webhooks.
Google Chat
- Open Google Chat in a browser and go to the space.
- Click the arrow beside the space’s name and choose Apps & integrations, then Add webhooks.
- Name the webhook and save it.
- Open its More menu and choose Copy link. The address starts
https://chat.googleapis.com/.
Webhooks in Google Chat need a Google Workspace Business or Enterprise account. If Add webhooks is not there, your organisation has not allowed them.
Google’s own guide: Build a Google Chat app as a webhook.
What a message says
Which project, what happened, who did it, the note’s type, the first 200 characters of the note or the reply, the page it is on, and a link to the note in your dashboard:
New note: Kestrel and Rye Bakery
Jess Halloran (jess@kestrelandrye.example) left note 7 (Wording) on /menu.
"Could this say "Baked fresh every morning" instead?"
Open the note: https://app.clicknote.io/admin/projects/12?c=345In Slack, Discord and Google Chat it is plain text. In Teams the same words arrive as a card, with a button that opens the note. Nothing a client types into a note can notify the whole channel, mention anybody or pass a link off as something else.
The outgoing webhook
Give ClickNote an https address of your own and it sends each event there as a POST with a JSON body. Choose A webhook of your own when you add a destination.
The address has to start https://, use a hostname and not an IP address, use the standard port, and belong to a server on the public internet. ClickNote checks that each time it sends, follows no redirect, and waits 10 seconds for an answer. Answer with any 2xx status to say you have it. Anything else counts as a failure and is tried again.
The signing secret
When you add a webhook, the page shows its signing secret once. Copy it then: it is not shown again. If it is lost, open Edit in the destination’s row and press Make a new secret, which shows a new one and stops the old one matching at once.
What arrives
POST /clicknote HTTP/1.1
Host: hooks.youragency.example
Content-Type: application/json
User-Agent: ClickNote-Webhook/1
ClickNote-Event: evt_a6d9f2b022ae2491b3957795
ClickNote-Delivery: dlv_4d0594e57c75f8ec873905cd
ClickNote-Signature: t=1794646519,v1=3a7b6573570bae7eb758f49ea76589e920a1d3bdc17bf82073f801aab7714055ClickNote-Eventis the event’s id, the same asidin the body. It is the same on every retry, and at every destination the event goes to.ClickNote-Deliveryis that event at this destination. It is also the same on every retry, so if you have seen it before you can drop the repeat.ClickNote-Signaturecarries the time the request was sent and a signature of the body. The next section checks it.
The three events
- note.created
- A client has left a note. Its
dataholdsnote(the note),note_url(the note in your dashboard) andreviewer(who left it, with their name from the project’s People list if they are on it). - round.finished
- A client pressed “I’ve finished this round”. A second press within ten minutes is the same round and sends nothing. Its
dataholdsreviewer(who finished),counts(how many of the project’s notes are open, in progress and done) andround(the number of the round they finished). - reply.created
- Someone replied in a note’s thread: a client, or your team from the dashboard. Its
dataholds the samenoteandnote_url, andreply: its text, its author, who it mentions and when it was written. The author’sfromsaysreviewerfor a client andagencyfor your team.
Every event has the same outside: id, type, created (the time it happened) and project (its id, its name, the site’s address and its page in your dashboard). What is in data depends on the type. This is a whole note.created:
{
"id": "evt_a6d9f2b022ae2491b3957795",
"type": "note.created",
"created": "2026-11-14T08:55:19.716Z",
"project": {
"id": 12,
"name": "Kestrel and Rye Bakery",
"url": "https://www.kestrelandrye.example/",
"dashboard_url": "https://app.clicknote.io/admin/projects/12"
},
"data": {
"note": {
"id": 345,
"number": 7,
"status": "open",
"type": "copy",
"page_url": "https://www.kestrelandrye.example/menu",
"page_title": "Menu - Kestrel and Rye Bakery",
"selector": "#hero h1",
"xpath": "/html/body/main/section[1]/div/h1",
"fingerprint": {
"tag": "h1",
"text": "bread worth getting up for",
"chain": ["body", "main", "section", "div"],
"landmark": "main",
"classes": ["hero__title"]
},
"tag": "h1",
"element_text": "Bread worth getting up for",
"element_html": "<h1 class=\"hero__title\">Bread worth getting up for</h1>",
"rect": { "x": 96, "y": 212, "w": 560, "h": 136 },
"viewport": { "w": 1440, "h": 900, "dpr": 2 },
"ua": { "browser": "Chrome 142", "os": "macOS", "device": "desktop" },
"context": {
"platform": {
"kind": "wordpress",
"theme": "kestrel",
"template": "page-template-menu"
},
"trail": ["Gutenberg block core/heading"],
"dom_path": "main#content > section.hero > div.hero__inner > h1.hero__title",
"attrs": {},
"styles": {
"font-size": "56px",
"font-weight": "700",
"color": "rgb(43, 33, 24)"
},
"heading": "Bread worth getting up for",
"lang": "en-GB",
"scroll_y": 0,
"click": { "x": 0.42, "y": 0.5 },
"device": {
"screen": "1512x982",
"layout": "desktop",
"pointer": "mouse",
"color_scheme": "light"
}
},
"comment": "Could this say \"Baked fresh every morning\" instead?",
"internal_note": null,
"reviewer_email": "jess@kestrelandrye.example",
"screenshot_url": "https://app.clicknote.io/s/345.webp?e=1794732919&sig=Zcvgrc1G4Kn6_dX6UoWug8pSGgg47w875GNtO5lBFRo",
"screenshot_failed": false,
"created_at": "2026-11-14T08:55:19.716Z",
"updated_at": "2026-11-14T08:55:19.716Z",
"deleted_at": null,
"round": 2
},
"note_url": "https://app.clicknote.io/admin/projects/12?c=345",
"reviewer": { "email": "jess@kestrelandrye.example", "name": "Jess Halloran" }
}
}The note is in the same form as the dashboard’s JSON export. Its round is the number of the round of feedback it was left in: a project starts in round 1, and your team starts the next round from the project’s Review link and people page when the changes are made and the client should look again. screenshot_url is a signed link to the image, not the image itself, and works for 24 hours from when the delivery was sent. A note’s screenshot is uploaded a moment after the note, so note.created waits for it, 20 seconds at most; if none has arrived by then, screenshot_url is null.
A reply’s data has the same note and note_url, and beside them:
{
"reply": {
"id": 88,
"text": "Could it go live by Friday?",
"author": {
"name": "Jess Halloran",
"email": "jess@kestrelandrye.example",
"from": "reviewer"
},
"mentions": ["agency"],
"created_at": "2026-11-14T09:12:44.512Z"
}
}And a finished round’s data is:
{
"reviewer": { "email": "jess@kestrelandrye.example", "name": "Jess Halloran" },
"counts": { "open": 4, "in_progress": 1, "done": 7 },
"round": 2
}What Send a test event sends is a note.created with one more field at the top, "test": true, a made-up note that says it is a test, and an id starting evt_test_. Real events never have a test field, so skip the ones that do in anything that acts on a note.
Check the signature
The header looks like t=1794646519,v1=3a7b.... t is the time the request was sent, in seconds. v1 is an HMAC-SHA256, in hex, of the time, a full stop and the raw body, keyed with your signing secret exactly as it was shown to you. To check a request, work the same sum out yourself and compare, and refuse anything more than five minutes old:
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: the body exactly as it arrived, before any JSON parsing.
// header: the value of the ClickNote-Signature header.
// secret: the signing secret, exactly as it was shown to you.
function isFromClickNote(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((part) => part.split('=')));
const recent = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
const given = Buffer.from(String(parts.v1));
return recent && given.length === expected.length && timingSafeEqual(given, Buffer.from(expected));
}Use the body exactly as it arrived. If your framework has already parsed the JSON and you build the string again from that, the signature will not match.
When the other end is down
Nothing about a destination holds up your clients. A note, a reply and a finished round are saved first, and telling the destinations happens afterwards. If a destination does not accept a delivery, it is tried again:
| When | What happens |
|---|---|
| Straight away | The first try, in the background, as soon as the note, reply or round is saved. |
| After 1 minute | The second try. |
| After a further 5 minutes, then 30 minutes, then 2 hours | The third, fourth and fifth. |
| Then every 6 hours | Until the delivery is 24 hours old. The last try is at 24 hours, and then it is given up on. |
- On the Notifications page. Each destination shows when it last worked. One that is failing shows since when, and what the other end last answered: a status and a few words.
- After three days of nothing but failures a destination is switched off, and the workspace’s owner is emailed once. Put the address right, or add a new one, then press Switch on again.
- A ceiling. One destination is sent at most 120 deliveries an hour, and a whole workspace at most 1,000 an hour across all its projects, or 300 during the trial. Anything beyond that is not sent, and the page says how many were left out.
Because a delivery can be retried, your receiver may see the same one twice: once when it answered too slowly, and again a minute later. Keep the ClickNote-Delivery ids you have handled and ignore one you have seen.
Jira, Asana, Trello, Linear and ClickUp
ClickNote has no built-in integration with Jira, Asana, Trello, Linear or ClickUp. The webhook is how a note reaches them: an automation tool gives you an address to paste in as the webhook, receives each event and creates the task. In Zapier that is the Catch Hook trigger of Webhooks by Zapier, in Make it is a custom webhook, and in n8n it is a Webhook node. Add the destination, press Send a test event so the tool has a note to look at, then map data.note.comment, data.note.page_url, data.note_url and whatever else you want onto the task’s fields.
New to ClickNote? Try it on a sample site first. No sign-up for the demo, no card for the 14-day trial. See pricing.
Try the live demo