Recibir webhooks
Haz que los elementos nuevos lleguen a tu propio extremo en cuanto se guardan. Que un elemento pase a ser buscable (o no lo consiga) y que un elemento se borre no se pueden saber por sondeo y solo llegan por esta vía. El destino se configura en tu página de cuenta.
Cabeceras
Se envían en cada entrega. Verificarlas es opcional.
| Cabecera | Ejemplo | Qué es |
|---|---|---|
webhook-id | msg_2KWPBg… | Clave de idempotencia. Idéntica en todos los reintentos de un evento. |
webhook-timestamp | 1614265330 | Segundos unix. Comprueba que sea reciente para rechazar reenvíos. |
webhook-signature | v1,g0hM9SsE… | HMAC-SHA256 del cuerpo, en base64. |
content-type | application/json | Siempre este valor. |
| tu nombre de cabecera de autenticación | tu valor de cabecera de autenticación | También se envía, si registraste una. |
POST <your endpoint>
content-type: application/json
webhook-id: msg_2KWPBgLlAfxdpx2AI54pPJ85f4W
webhook-timestamp: 1614265330
webhook-signature: v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=
Cuerpo
El sobre no cambia nunca; type decide la forma de data.
| type | data | Cuándo |
|---|---|---|
note.created | un elemento, con su contenido | Se guarda un elemento. No significa que sea buscable: mira status. |
note.ready | { count, group_id?, items[] } | Elementos guardados pasan a ser buscables. Un evento por grupo y por pasada de procesamiento. |
note.failed | { count, group_id?, items[] } | Elementos guardados no se pudieron hacer buscables. Siguen ahí. |
note.deleted | { count, items[{id}] } | Se eliminan elementos. Solo ids: ya no existen. |
webhook.ping | {} | Pulsas Probar la conexión. |
Un guardado suelto
{
"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
}
}
Una parte de una subida
{
"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"
}
}
Partes de una subida pasaron a ser buscables
{
"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" }
]
}
}
Partes de una subida no se pudieron hacer buscables
{
"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" }
]
}
}
Un archivo puede quedar en parte buscable y en parte no: entonces ambos eventos llegan con segundos de diferencia y el mismo group_id.
Se borraron elementos
{
"type": "note.deleted",
"timestamp": "2026-09-05T05:02:44Z",
"data": { "count": 2, "items": [{ "id": 42 }, { "id": 43 }] }
}
La prueba de conexión
{
"type": "webhook.ping",
"timestamp": "2026-09-05T05:10:00Z",
"data": {}
}
Guardados sueltos y subidas
Los dos flujos usan los mismos tipos de evento; status y group_id te dicen cuál tienes.
Un guardado suelto: desde MCP, la API REST o tu panel. Un solo note.created, y ya es buscable en cuanto llega.
Una subida: el archivo se divide en partes y cada parte recibe su propio note.created. Ninguna es buscable todavía; note.ready llega después, cuando ya lo son.
| Un guardado suelto | Una subida (un archivo) | |
|---|---|---|
note.created | uno, status: "ready", group_id: null | uno por parte, status: "pending", con group_id |
note.ready | no llega nunca | llega cuando las partes pasan a ser buscables |
note.failed | no llega nunca | llega si alguna parte no se pudo hacer buscable |
| Buscable desde | note.created | note.ready |
| Legible desde | de inmediato | de inmediato: lo único que espera es la búsqueda |
| Número de parte | — | no es un campo. Solo el (3/77) del título y el orden de data.id |
status: si el elemento es buscable."ready"significa que sí;"pending", que aún se está preparando;"failed", que su preparación falló y la búsqueda no lo encontrará.group_id: el nombre de un lote. Se emite al subir un archivo y todas las partes de ese archivo llevan el mismo valor. Dice a qué archivo pertenece una parte, no qué número de parte es, y es la clave que enlaza unnote.createdcon elnote.readyque viene después.- Si solo quieres elementos que puedas buscar: actúa con
note.readyy salta cualquiernote.createdcuyostatussea"pending". - Una parte pendiente se puede leer de inmediato: su contenido ya viene en la carga. Para volver a leerlo, usa
GET /api/v1/notes/{id}, que no cuenta como apertura, oGET /api/v1/notes/group/{group_id}para el lote entero. note.readyno llega una vez por archivo: cada uno lleva las partes que quedaron listas a la vez, así que un archivo cuyo procesamiento se divide, o una parte que hubo que reintentar, produce varios eventos con el mismogroup_id.countes lo que llevaba ese evento, no el total del archivo, así que acumulaitems.note.failedno significa que el elemento se haya borrado: sigue guardado y se puede leer, y solo falló su preparación para la búsqueda. No lo quites de tu copia; el borrado tiene su propio evento,note.deleted. Pulsa Reintentar en tu panel para prepararlo de nuevo, y si vuelve a fallar llegará unnote.failednuevo.
Cómo escribir el receptor
Responde 2xx de inmediato y haz el trabajo después, en tu propia cola.
| Haz esto | Si no |
|---|---|
Devuelve 2xx ya, procesa después | A los 25 segundos damos por fallida la entrega y la repetimos. |
Descarta duplicados con webhook-id | Procesarás el mismo elemento dos veces. |
Responde a un duplicado con 200, nunca 429 | 429 significa «más despacio», así que lo reenviamos. |
Ordena por data.id | Un reintento puede adelantar a un evento posterior; el orden no está garantizado. |
| Registra la URL final | No seguimos redirecciones: un 3xx termina la entrega. |
Verificar la firma (opcional)
Confirma que la entrega viene de nosotros y que el cuerpo no se alteró por el camino.
- Construye la cadena firmada:
{webhook-id}.{webhook-timestamp}.{cuerpo en bruto} - Obtén la clave: quita el prefijo
whsec_del secreto y descodifica el resto en base64 - HMAC-SHA256, codifica en base64 y compara con la parte que sigue a
v1, - Comprueba que
webhook-timestampesté dentro de unos minutos respecto a ahora - Verifica los bytes en bruto que recibiste: analizar el JSON y volver a serializarlo rompe la firma
- Cualquier biblioteca de Standard Webhooks (standardwebhooks.com) hace los pasos 1 a 5 por ti
Tu secreto de firma está en la página de cuenta, donde puedes copiarlo o cambiarlo.
Reintentos
Si una entrega falla la enviamos hasta tres veces más (cuatro intentos en total). Solo 2xx cuenta como entregado.
| Respuesta | Qué hacemos |
|---|---|
2xx | Listo. |
408 · 429 · 5xx | Reintentamos a los 15 minutos, luego a la hora y luego a las 3 horas. |
| Ninguna respuesta | Reintentamos con el mismo calendario. |
3xx y cualquier otro 4xx | Paramos aquí: la respuesta no va a cambiar. |
Las entregas nunca se desactivan solas. Mientras siga fallando te avisamos por correo como mucho una vez al día, y tu página de cuenta muestra la racha de fallos y el último error.
Si tu receptor estuvo caído más de unas cuatro horas, ponte al día con GET /api/v1/notes/changes.
Más
Registrar un destino, la clave de firma y la prueba de conexión están todos en tu página de cuenta. Las claves, los extremos y los errores de la API REST están en la documentación de la API REST. Las preguntas frecuentes tienen respuesta en las FAQ y, si algo no queda claro o no funciona, escríbenos: una persona real lee todos los mensajes.