Skip to content

Integrations

Webhooks

Every comment event sent to your own URL as signed JSON, for Zapier, Make, n8n or your own server.

A webhook posts every comment event to a URL of your own as signed JSON, as it happens. Use it to put new comments in another tool through Zapier, Make or n8n, or to handle them on your own server.

Webhooks come with the Studio and Team plans, and the workspace owner and admins manage them on the Integrations page in the sidebar. Just want new comments in a channel? Slack does that without any setup on your side.

Add a webhook

On the Integrations page, click Add webhook under Webhooks and fill in:

  • URL: where layernote posts. It has to start with https://, use the standard port and be reachable from the internet. Redirects are not followed, so use the final address.
  • Description: a note for yourself, for example "New comments to our task board".
  • Events: which events to send. All of them are on to begin with.
  • Projects: all projects, or the ones you choose.

When you save, layernote shows the webhook's signing secret once. Copy it into your receiver right away: you need it to verify the signature. A workspace can have up to 10 webhooks.

Events

EventSent when
comment.createdA new comment is placed
comment.repliedSomeone replies to a comment
comment.screenshot_readyThe screenshot of a comment is ready
comment.resolvedA comment is resolved, also by dragging it to Resolved on the board
comment.reopenedA resolved comment is opened again
comment.status_changedIts status or board column changes
comment.assignedSomeone is assigned to it
comment.unassignedIts assignee is removed
comment.updatedIts text, priority, category or due date changes
comment.deletedA comment or reply is deleted

Send test on a webhook sends a ping event.

Payload

Every event has the same shape:

{
  "id": "evt_9c1f0e5b7a2d4c6e8f0a1b2c3d4e5f60",
  "type": "comment.created",
  "createdAt": "2026-10-26T09:14:02.511Z",
  "workspace": { "id": "ws_1" },
  "project": { "id": "p1", "name": "Spring campaign" },
  "comment": {
    "id": "ann_1",
    "parentId": null,
    "text": "The bottle looks blue here. It should be the green from the brand guide.",
    "status": "OPEN",
    "resolved": false,
    "boardColumn": "Open",
    "priority": "HIGH",
    "category": "DESIGN",
    "dueDate": null,
    "createdAt": "2026-10-26T09:14:01.000Z",
    "url": "https://app.layernote.io/projects/p1/live?annotationId=ann_1",
    "location": {
      "kind": "image",
      "name": "hero-banner.png",
      "fileVersion": 2,
      "pin": { "xPercent": 40, "yPercent": 60 }
    },
    "screenshot": { "status": "pending", "url": null },
    "author": { "name": "Amelia Moreau", "email": "amelia@example.com", "isGuest": false },
    "assignee": null,
    "labels": ["Copy"],
    "source": "dashboard"
  },
  "actor": { "name": "Amelia Moreau", "email": "amelia@example.com" },
  "changed": [],
  "previous": null
}
  • comment.url opens the comment in layernote. For a reply it opens the comment the reply belongs to.
  • location says where the comment is. On a live page, a video or a PDF it also carries the page address, device, element, nearest heading, moment or page number.
  • screenshot.status is pending until the picture is made, then ready with a url; comment.screenshot_ready tells you when. A reply has none.
  • changed and previous say what changed and what it was before, for example the old status and column on comment.resolved.
  • author.email is null for a reviewer from a share link.
  • comment.deleted carries only id, parentId and "deleted": true.

The event describes the comment as it is when it is sent, so a retry carries the latest state.

Verify the signature

layernote signs every request the Standard Webhooks way, with three headers:

HeaderWhat it holds
webhook-idThe event's id. The same on every retry, so use it to skip duplicates.
webhook-timestampWhen it was sent, in seconds since 1970
webhook-signaturev1, and a base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}

The key is your signing secret without its whsec_ prefix, base64-decoded. Any Standard Webhooks library checks this for you. Or in Node:

import crypto from "node:crypto";

// rawBody: the request body exactly as it arrived, as a string.
function verifyLayernoteWebhook(headers, rawBody, secret) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = String(headers["webhook-signature"] ?? "").split(" ");

  // Refuse anything older or newer than 5 minutes.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = crypto
    .createHmac("sha256", key)
    .update(`${id}.${timestamp}.${rawBody}`)
    .digest("base64");

  return signatures.some((entry) => {
    const [version, signature] = entry.split(",");
    return (
      version === "v1" &&
      signature?.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
    );
  });
}

Check the body exactly as it arrived, before parsing it: parsing and writing JSON back out changes the bytes.

Lost the secret, or think someone else has it? Open the webhook and click Make new secret. The old one stops working at once, so update your receiver right after.

Deliveries and retries

  • Answer with any 2xx status within 10 seconds. Anything else counts as failed.
  • A failed delivery is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours, with the same webhook-id.
  • When 5 deliveries in a row fail for good, layernote switches the webhook off and emails the person who added it. Fix the receiver, check it with Send test, then Turn on.
  • Recent deliveries on a webhook shows the last 50, with the status code, attempts and error. A failed one has Resend. Deliveries are kept for 30 days.