Receiving webhooks
Have new items pushed to your own endpoint the moment they are saved. An item becoming searchable (or failing to), and an item being deleted, cannot be learned by polling and arrive only this way. Set the destination up on your account page.
Headers
Sent on every delivery. Verifying them is optional.
| Header | Example | What it is |
|---|---|---|
webhook-id | msg_2KWPBg… | Idempotency key. Identical across all retries of one event. |
webhook-timestamp | 1614265330 | Unix seconds. Check it is recent to reject replays. |
webhook-signature | v1,g0hM9SsE… | HMAC-SHA256 of the body, base64. |
content-type | application/json | Always this. |
| your auth header name | your auth header value | Sent too, if you registered one. |
POST <your endpoint>
content-type: application/json
webhook-id: msg_2KWPBgLlAfxdpx2AI54pPJ85f4W
webhook-timestamp: 1614265330
webhook-signature: v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=
Body
The envelope never changes; type decides the shape of data.
| type | data | When |
|---|---|---|
note.created | one item, including its content | An item is stored. Not the same as searchable — read status. |
note.ready | { count, group_id?, items[] } | Stored items become searchable. One event per group, per processing pass. |
note.failed | { count, group_id?, items[] } | Stored items could not be made searchable. They are still there. |
note.deleted | { count, items[{id}] } | Items are removed. Ids only — they are already gone. |
webhook.ping | {} | You press Test connection. |
A single save
{
"type": "note.created",
"timestamp": "2026-09-05T04:12:07Z",
"data": {
"id": 41,
"title": "…",
"summary": "…",
"content": "…",
"created_at": "2026-09-05T04:11:58Z",
"origin": "Claude",
"status": "ready",
"group_id": null
}
}
One part of an upload
{
"type": "note.created",
"timestamp": "2026-09-05T04:12:09Z",
"data": {
"id": 42,
"title": "… (3/77, p.47–53)",
"summary": "…",
"content": "…",
"created_at": "2026-09-05T04:12:08Z",
"origin": "Upload",
"status": "pending",
"group_id": "9f2c1ab84e7d4f0b93c5a61d2e8b7c40"
}
}
Parts of an upload became searchable
{
"type": "note.ready",
"timestamp": "2026-09-05T04:31:12Z",
"data": {
"count": 2,
"group_id": "9f2c1ab84e7d4f0b93c5a61d2e8b7c40",
"items": [
{ "id": 42, "title": "… (3/77, p.47–53)", "status": "ready" },
{ "id": 43, "title": "… (4/77, p.54–60)", "status": "ready" }
]
}
}
Parts of an upload could not be made searchable
{
"type": "note.failed",
"timestamp": "2026-09-05T04:36:40Z",
"data": {
"count": 2,
"group_id": "9f2c1ab84e7d4f0b93c5a61d2e8b7c40",
"items": [
{ "id": 44, "title": "… (5/77, p.61–67)", "status": "failed" },
{ "id": 45, "title": "… (6/77, p.68–74)", "status": "failed" }
]
}
}
One file can end up partly searchable and partly not: both events then arrive seconds apart with the same group_id.
Items were deleted
{
"type": "note.deleted",
"timestamp": "2026-09-05T05:02:44Z",
"data": { "count": 2, "items": [{ "id": 42 }, { "id": 43 }] }
}
The connection test
{
"type": "webhook.ping",
"timestamp": "2026-09-05T05:10:00Z",
"data": {}
}
Single saves and uploads
Both flows use the same event types; status and group_id tell you which one you have.
A single save — from MCP, the REST API or your dashboard. One note.created, and it is searchable the moment it arrives.
An upload — the file is split into parts, and every part gets its own note.created. None of them is searchable yet; note.ready follows once they are.
| A single save | An upload (one file) | |
|---|---|---|
note.created | one, status: "ready", group_id: null | one per part, status: "pending", with a group_id |
note.ready | never arrives | arrives as parts become searchable |
note.failed | never arrives | arrives if a part could not be made searchable |
| Searchable from | note.created | note.ready |
| Readable from | straight away | straight away — only search waits |
| Part number | — | not a field. Only the (3/77) in the title, and the order of data.id |
status— whether the item is searchable."ready"means it is;"pending"means it is still being prepared;"failed"means preparing it did not succeed, so search will not find it.group_id— the name of a bundle. It is issued when you upload a file and every part of that file carries the same value. It says which file a part belongs to, not which part it is, and it is the key that links anote.createdto thenote.readythat follows.- If you only want items you can search — act on
note.readyand skip anynote.createdwhosestatusis"pending". - A pending part is readable straight away — its body is already in the payload. To read it again use
GET /api/v1/notes/{id}, which does not count as an opening, orGET /api/v1/notes/group/{group_id}for the whole bundle. note.readydoes not arrive once per file — each one carries the parts that became ready together, so a file whose processing is split, or a part that had to be tried again, produces several events with the samegroup_id.countis what that event carried, not the file's total, so accumulateitems.note.faileddoes not mean the item was deleted — it is stored and readable, and only preparing it for search failed. Do not remove it from your copy; deletion has its own event,note.deleted. Press Retry on your dashboard to try again, and if that attempt fails too a newnote.failedarrives.
Writing a receiver
Answer 2xx straight away and do the work afterwards, on your own queue.
| Do this | Or else |
|---|---|
Return 2xx immediately, process later | We time out at 25 seconds and deliver again. |
Deduplicate on webhook-id | You process the same item twice. |
Answer a duplicate with 200, never 429 | 429 means “slow down”, so we send it again. |
Sort by data.id | A retry can overtake a later event; order is not guaranteed. |
| Register the final URL | We do not follow redirects — a 3xx ends the delivery. |
Verifying the signature (optional)
Confirms the delivery came from us and that the body was not altered on the way.
- Build the signed string:
{webhook-id}.{webhook-timestamp}.{raw body} - Derive the key: strip the
whsec_prefix from your secret, then base64-decode the rest - HMAC-SHA256, base64-encode, and compare against the part after
v1, - Check
webhook-timestampis within a few minutes of now - Verify the raw bytes you received — parsing the JSON and re-serialising it breaks the signature
- Any Standard Webhooks library (standardwebhooks.com) does steps 1–5 for you
Your signing secret is on the account page, where you can copy or replace it.
Retries
If a delivery fails we send it up to three more times — four attempts in all. Only 2xx counts as delivered.
| Response | What we do |
|---|---|
2xx | Done. |
408 · 429 · 5xx | Retry after 15 minutes, then 1 hour, then 3 hours. |
| No response at all | Retry on the same schedule. |
3xx and every other 4xx | Stop here — the answer will not change. |
Deliveries are never switched off automatically. While it keeps failing we email you at most once a day, and your account page shows the failure run and the last error.
If your receiver was down for more than about four hours, catch up with GET /api/v1/notes/changes.
More
Registering a destination, the signing secret and the connection test are all on your account page. Keys, endpoints and errors for the REST API are in the REST API docs. Common questions are answered in the FAQ, and if something is unclear or broken, get in touch — a real person reads every message.