Webhooks para desarrolladores
Disponible enFreeProResearch
Un webhook hace que Readsira envíe un POST HTTP a tu servidor cuando ocurre algo en tu cuenta, por
ejemplo cuando guardas un marcador. Esta página es para el código que lo recibe: los eventos, la
carga, la firma y las reglas de reintento. Para añadir un webhook en la aplicación, consulta
Webhooks.
Antes de empezar
- Una dirección
httpohttpspública que aceptePOST. Readsira rechazalocalhosty las direcciones de redes privadas, y no sigue redirecciones. - Tu servidor debe leer el cuerpo bruto de la solicitud. Necesitas los bytes exactos para comprobar la firma.
Cómo es una entrega
Cada entrega es un POST con un cuerpo JSON y tres cabeceras:
| Cabecera | Valor |
|---|---|
content-type | application/json |
x-readsira-event | El nombre del evento, por ejemplo bookmark.created |
x-readsira-signature | sha256= seguido del HMAC del cuerpo, en hexadecimal |
El cuerpo es un sobre:
{
"event": "bookmark.created",
"timestamp": "2026-11-04T09:15:02.481Z",
"data": {
"bookmark_id": "c71d0e3a-5b1c-4c58-9d6f-0f6c0d2a9e11",
"user_id": "7b0e1f42-2a43-4f0b-8f0e-1d2c3b4a5f60",
"url": "https://example.com/grid-lab",
"title": "Grid Lab: long-duration storage",
"tags": ["energy"],
"collection_id": "a2c4e6f8-1b3d-4f5a-8c7e-9d0b1a2c3d4e"
}
}
timestamp es el momento en que Readsira construyó la entrega. Los valores de arriba son un ejemplo.
Eventos
Tú eliges los eventos de cada webhook. Un webhook nuevo recibe bookmark.created.
| Evento | Se envía cuando | data |
|---|---|---|
bookmark.created | Guardas un marcador | bookmark_id, user_id, url, title, tags, collection_id |
bookmark.updated | Cambias un marcador | bookmarkId, userId, bookmark, changes, notesChanged |
bookmark.deleted | Se elimina un marcador | bookmarkId, userId, bookmark |
bookmark.visited | Abres un marcador | bookmarkId, userId, visitedAt |
output.created | Termina un resultado de proyecto | id, projectId, kind, title, version, sourceCount, completedAt |
La aplicación también enumera collection.created, collection.updated y collection.deleted.
Readsira todavía no los envía, así que un webhook que solo escucha estos eventos no recibe nada.
En bookmark.updated y bookmark.deleted, bookmark es una instantánea con id, url, title,
description, collectionId, tags, isStarred, isPublic, domain, notes, triageStatus y
captureIntent. changes enumera los nombres de los campos que fijó la solicitud. En
output.created la carga lleva solo ids y metadatos, nunca el texto del resultado; léelo en la
aplicación.
El botón Probar de la aplicación envía un evento más, webhook.test, con
data: { "message": "Test webhook from Readsira", "userId": "..." }. Está firmado igual que los demás.
Los webhooks se pausan mientras tu cuenta está programada para eliminarse.
Comprobar la firma
La firma demuestra que la solicitud vino de Readsira y que nadie cambió el cuerpo. Readsira calcula
HMAC-SHA256 sobre el cuerpo bruto, con el secreto del webhook como clave, y envía el resultado como
sha256= más 64 caracteres hexadecimales.
El secreto es la cadena hexadecimal de 64 caracteres que muestra la aplicación cuando creas el webhook o rotas su secreto. Usa esa cadena tal cual, como texto. No la decodifiques desde hexadecimal. Se muestra una sola vez.
Un receptor en Node, sin paquetes:
import { createHmac, timingSafeEqual } from 'node:crypto';
import { createServer } from 'node:http';
const SECRET = process.env.READSIRA_WEBHOOK_SECRET;
function isFromReadsira(rawBody, signatureHeader) {
const expected = 'sha256=' + createHmac('sha256', SECRET).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader ?? '');
return a.length === b.length && timingSafeEqual(a, b);
}
createServer((req, res) => {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
const rawBody = Buffer.concat(chunks);
if (!isFromReadsira(rawBody, req.headers['x-readsira-signature'])) {
res.writeHead(401).end();
return;
}
const { event, timestamp, data } = JSON.parse(rawBody.toString('utf8'));
console.log(event, timestamp, data);
res.writeHead(204).end();
});
}).listen(3000);
Tres detalles que causan la mayoría de los fallos:
- Calcula el hash sobre los bytes brutos. Si tu framework analiza primero el JSON y lo serializas de
nuevo, los bytes difieren y la comprobación falla. En Express, usa
express.raw({ type: 'application/json' })para la ruta del webhook. - Compara en tiempo constante, como hace
timingSafeEqual, no con===. - Después de rotar el secreto en la aplicación, las solicitudes firmadas con el secreto antiguo no
pasan la comprobación. Actualiza
READSIRA_WEBHOOK_SECRETprimero.
Para rechazar repeticiones, compara timestamp con el reloj y descarta las entregas que tengan más de
unos minutos. La marca de tiempo está dentro del cuerpo firmado, así que no se puede cambiar sin romper
la firma. Un reintento (abajo) lleva de nuevo el evento original, así que haz que tu gestor sea seguro
si se ejecuta dos veces.
Responder rápido
Responde con cualquier estado 2xx en cuanto hayas guardado el evento. Readsira espera 5 segundos y
lee como máximo 64 KB de tu respuesta.
| Tu respuesta | Qué hace Readsira |
|---|---|
2xx | La entrega cuenta como entregada. |
5xx o ninguna respuesta en 5 segundos | La entrega se reintenta, hasta 4 intentos en total, con una espera entre ellos que empieza en 30 segundos y se duplica. |
4xx | La entrega cuenta como fallida y no se reintenta. |
La aplicación muestra para cada webhook cuántas veces se activó y cuándo fue la última vez.
Consejos
- Mantén el gestor corto. Pon el trabajo en una cola y responde primero.
- Usa el botón Probar mientras desarrollas, antes de guardar un marcador real.
- Guarda el secreto como una contraseña. Rótalo si se filtra.
Relacionado
- Webhooks (se configura en la aplicación)
- Ejemplos
- Límites de uso y errores