Saltar al contenido principal

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 http o https pública que acepte POST. Readsira rechaza localhost y 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:

CabeceraValor
content-typeapplication/json
x-readsira-eventEl nombre del evento, por ejemplo bookmark.created
x-readsira-signaturesha256= 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.

EventoSe envía cuandodata
bookmark.createdGuardas un marcadorbookmark_id, user_id, url, title, tags, collection_id
bookmark.updatedCambias un marcadorbookmarkId, userId, bookmark, changes, notesChanged
bookmark.deletedSe elimina un marcadorbookmarkId, userId, bookmark
bookmark.visitedAbres un marcadorbookmarkId, userId, visitedAt
output.createdTermina un resultado de proyectoid, 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_SECRET primero.

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 respuestaQué hace Readsira
2xxLa entrega cuenta como entregada.
5xx o ninguna respuesta en 5 segundosLa entrega se reintenta, hasta 4 intentos en total, con una espera entre ellos que empieza en 30 segundos y se duplica.
4xxLa 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.