Skip to content

Webhooks

LunaNotes sends a signed HTTP POST to your endpoint when something happens in a workspace — a note is generated, a meeting is ready, a page is saved. Deliveries follow the Standard Webhooks spec, so any of its libraries verifies them.

Plus plan

Webhooks are part of Plus, judged by the workspace owner's plan. Workspace owners and admins add endpoints in Workspace settings → Webhooks. If the owner leaves Plus, endpoints pause and resume when they come back.

View Pricing

Set up an endpoint

  1. Open your workspace's settings, then Webhooks → Add endpoint.
  2. Enter an https:// URL that is reachable from the internet. Private and local addresses are refused.
  3. Pick the events it should receive, or all of them.
  4. Copy the signing secret (whsec_…). You can reveal it again or rotate it later.
  5. We send a webhook.test event right away and show the result. Use Send test event any time.

Events

An endpoint only receives what its creator can see: never a note they have no access to, and meetings only from their own account. Endpoints created by someone who left the workspace stop receiving events.

TypeWhen
note.createdA note was created. AI-generated notes fire once their content is ready.
note.updatedA note's title, content or tags changed. Edits while someone types are batched.
note.deletedA note was moved to the trash.
note.restoredA note came back from the trash.
video.createdA video was added to the workspace, including channel imports.
video.deletedA video was removed.
meeting.readyA meeting's transcript, summary, decisions and action items are ready.
meeting.failedA meeting could not be recorded or processed.
meeting.deletedA meeting was deleted.
bookmark.createdA page was saved.
bookmark.updatedA saved page was enriched, renamed, re-filed or re-tagged.
bookmark.deletedA saved page was deleted.
webhook.testA test event, sent on demand and when an endpoint is created.

The request

Headers

POST /your/endpoint HTTP/1.1
content-type: application/json
user-agent: LunaNotes-Webhooks/1
webhook-id: evt_6c1f…            # same on every retry: use it to deduplicate
webhook-timestamp: 1791892800     # unix seconds of this attempt
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=

Body

Every event has the same envelope. data carries the whole resource; deletions carry only { "id" }. A data over 256 KB (a very long transcript) is replaced by { "id", "appUrl", "truncated": true }.

{
  "type": "note.created",
  "timestamp": "2026-10-13T12:00:00.000Z",
  "data": {
    "id": "6f0e2c1a-…",
    "workspaceId": "a1b2…",
    "title": "Photosynthesis",
    "markdown": "# Photosynthesis\n\nPlants turn light into …",
    "status": "ACTIVE",
    "authorId": "…",
    "videoId": null,
    "tags": [{ "id": "…", "label": "biology" }],
    "appUrl": "https://app.lunanotes.io/notes/6f0e2c1a-…",
    "createdAt": "2026-10-13T11:59:58.000Z",
    "updatedAt": "2026-10-13T11:59:58.000Z"
  }
}

data by resource

note

{ "id", "workspaceId", "title", "markdown", "status", "authorId",
  "videoId", "tags": [{ "id", "label" }], "appUrl", "createdAt", "updatedAt" }

video

{ "id", "workspaceId", "title", "youtubeId", "youtubeUrl", "channelId",
  "channelTitle", "thumbnailUrl", "durationSeconds", "appUrl", "createdAt" }

meeting

{ "id", "title", "platform", "meetingUrl", "status", "failureReason",
  "startedAt", "endedAt", "durationMinutes", "summary", "shortSummary",
  "keyQuestions", "decisions", "actionItems", "chapters": [{ "title", "summary", "startTime" }],
  "transcript", "segments": [{ "speaker", "text", "startTime" }], "noteId", "appUrl",
  "createdAt" }

bookmark

{ "id", "workspaceId", "url", "title", "description", "note", "cover",
  "favicon", "site", "enrichment", "collectionId", "tags": [{ "id", "label" }],
  "noteId", "authorId", "appUrl", "createdAt", "updatedAt" }

A meeting's segments is its transcript split by speaker turn; startTime is seconds from the start of the recording. Both segments and startTime can be null on older meetings.

Verify the signature

Verify every request before trusting it, against the raw body — re-serialized JSON won't match. During a secret rotation the signature header carries two signatures, one per secret, for 24 hours; a match on either is valid.

import { Webhook } from "standardwebhooks";

const wh = new Webhook(process.env.LUNANOTES_WEBHOOK_SECRET); // "whsec_..."

app.post("/webhooks/lunanotes", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    // Throws if the signature or the timestamp (5 min tolerance) is wrong.
    event = wh.verify(req.body, req.headers);
  } catch {
    return res.status(400).end();
  }

  // Deliveries can repeat: skip ids you have already handled.
  const id = req.headers["webhook-id"];

  switch (event.type) {
    case "note.created":
      // event.data.markdown, event.data.title, ...
      break;
  }
  res.status(200).end();
});

Delivery and retries

  • • Any 2xx within 15 seconds is a success. Anything else — another status, a timeout, a redirect — is a failure. Redirects are not followed.
  • • A failed delivery is retried 7 times: 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, 10 hours after each failure — about a day and a half in total.
  • • Delivery is at least once and not ordered. Deduplicate on webhook-id and compare updatedAt rather than relying on arrival order.
  • • An endpoint that keeps failing for 5 days is turned off, and its creator gets an email. Re-enable it from settings once it's fixed.
  • • The delivery log keeps 30 days of requests and responses. Any delivery can be re-sent from there.