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 PricingSet up an endpoint
- Open your workspace's settings, then Webhooks â Add endpoint.
- Enter an https:// URL that is reachable from the internet. Private and local addresses are refused.
- Pick the events it should receive, or all of them.
- Copy the signing secret (
whsec_âŚ). You can reveal it again or rotate it later. - We send a
webhook.testevent 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.
| Type | When |
|---|---|
| note.created | A note was created. AI-generated notes fire once their content is ready. |
| note.updated | A note's title, content or tags changed. Edits while someone types are batched. |
| note.deleted | A note was moved to the trash. |
| note.restored | A note came back from the trash. |
| video.created | A video was added to the workspace, including channel imports. |
| video.deleted | A video was removed. |
| meeting.ready | A meeting's transcript, summary, decisions and action items are ready. |
| meeting.failed | A meeting could not be recorded or processed. |
| meeting.deleted | A meeting was deleted. |
| bookmark.created | A page was saved. |
| bookmark.updated | A saved page was enriched, renamed, re-filed or re-tagged. |
| bookmark.deleted | A saved page was deleted. |
| webhook.test | A 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();
});from standardwebhooks.webhooks import Webhook
wh = Webhook(os.environ["LUNANOTES_WEBHOOK_SECRET"]) # "whsec_..."
@app.post("/webhooks/lunanotes")
def lunanotes_webhook(request):
try:
event = wh.verify(request.body, dict(request.headers))
except Exception:
return Response(status=400)
if event["type"] == "meeting.ready":
action_items = event["data"]["actionItems"]
return Response(status=200)# Without a library: HMAC-SHA256 over "<webhook-id>.<webhook-timestamp>.<raw body>",
# keyed with the base64-decoded part of the secret after "whsec_".
SECRET_BYTES=$(echo -n "${SECRET#whsec_}" | base64 -d | xxd -p -c 256)
SIGNED="$WEBHOOK_ID.$WEBHOOK_TIMESTAMP.$BODY"
EXPECTED=$(echo -n "$SIGNED" | openssl dgst -sha256 -mac HMAC -macopt hexkey:$SECRET_BYTES -binary | base64)
# Compare "v1,$EXPECTED" against each space-separated entry of webhook-signature.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-idand compareupdatedAtrather 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.