A webhook sends every new comment, reply and change on a project to a URL you choose, as signed JSON. Use it to open tickets in your own tracker, feed a dashboard, or start an automation. Webhooks are part of Pro.
Add one
- Open the project's settings
Use Settings in the ⋯ on the project's card or the gear on its board, then choose Integrations and find Work tools. You need to be an owner or admin.
- Press Add beside Webhook
Enter your endpoint's URL and a name. It must be public https, on port 443 or 8443.
- Copy the signing secret
It is shown once. You can make a new one later, which stops the old one at once.
- Choose the events
Open What to send from the webhook's ⋯. New webhooks start with new comments, replies and status changes.
- Send a test
Your endpoint receives a
pingmade from the project's latest comment.
What it sends
| Event | When | Chosen as |
|---|---|---|
comment.created | A new comment | A new comment arrives |
comment.updated | Its words, priority, due date, tags, page, screenshot or files changed | Comes with new comments |
comment.deleted | It was deleted | Comes with new comments |
comment.replied | Someone replied | Someone replies |
comment.status_changed | Its status changed, including a resolve | The status changes |
comment.assigned | Someone was assigned | Someone is assigned (off to start with) |
ping | You pressed Send a test | Always |
A comment is saved before its screenshot has finished uploading, so comment.created usually has no picture yet. A comment.updated with { "kind": "screenshot" } in changes follows when it lands.
A deliveryPOST /your/endpoint
Content-Type: application/json; charset=utf-8
RMS-Event: comment.status_changed
RMS-Delivery: 0b6e…
RMS-Signature: t=1793700000,v1=5f2a…
{
"version": 1,
"id": "0b6e…",
"type": "comment.status_changed",
"occurred_at": "2026-11-05T09:12:44Z",
"project": { "id": "…", "name": "Acme website", "url": "…", "site_url": "https://acme.com" },
"comment": {
"id": "…", "number": 18, "text": "The checkout button overlaps the footer",
"status": { "key": "resolved", "label": "Resolved" }, "priority": "urgent",
"page": "Pricing", "page_url": "https://acme.com/pricing", "assignee": "Sam",
"due_on": "2026-11-12", "tags": ["Checkout"], "screenshot_url": "https://…",
"url": "…", "site_url": "…"
},
"actor": { "name": "Sam" },
"change": { "from": { "key": "open", "label": "Open" }, "to": { "key": "resolved", "label": "Resolved" } },
"reply": null
}Email addresses are never sent. The console and network logs are left out unless you tick them in What to send.
Check the signature
Your URL is public, so anyone could post to it. RMS-Signature proves a delivery came from us: v1 is the HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with your signing secret. Use the raw body before any JSON parsing, and refuse anything older than five minutes.
Nodeimport { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const want = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest();
const got = Buffer.from(parts.v1 ?? '', 'hex');
return got.length === want.length && timingSafeEqual(got, want);
}Pythonimport hmac, hashlib, time
def verify(secret: str, header: str, raw_body: bytes) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
want = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, parts.get("v1", ""))Answering, retries and pausing
- Accept with any 2xx, within 10 seconds. Do slow work after you answer.
- A timeout, 408, 429 or 5xx is tried again, up to five more times, with growing gaps. Retries go out with the project's next event or a daily sweep, so in a quiet project one can take until the next day.
- Anything else is not retried, a redirect included. Redirects are never followed.
- The same event can arrive twice.
RMS-Deliveryis the same on every retry, so use it to ignore repeats. - After 5 deliveries in a row have failed for good, the webhook pauses and the workspace owner gets an email. Resume it from its ⋯, or give it a new URL.
Recent posts, in the webhook's ⋯, shows what was delivered and what went wrong. Send a test is not listed there: its result shows straight away.
Using an automation tool
Zapier's Catch Hook, Make's custom webhook and n8n's webhook node all give you an https URL to paste here, which is a quick way to reach apps we do not connect to yet. They do not check the signature unless you add a step for it, so keep that URL private.