Webhooks für Entwickler
Verfügbar inFreeProResearch
Ein Webhook lässt Readsira einen HTTP-POST an deinen Server senden, wenn in deinem Konto etwas
passiert, zum Beispiel wenn du ein Lesezeichen speicherst. Diese Seite ist für den Code, der ihn
empfängt: die Ereignisse, die Payload, die Signatur und die Regeln für Wiederholungen. Um einen Webhook
in der App hinzuzufügen, siehe Webhooks.
Bevor du anfängst
- Eine öffentliche
http- oderhttps-Adresse, diePOSTannimmt. Readsira lehntlocalhostund Adressen im privaten Netzwerk ab und folgt keinen Weiterleitungen. - Dein Server muss den rohen Request-Body lesen. Du brauchst die exakten Bytes, um die Signatur zu prüfen.
Wie eine Zustellung aussieht
Jede Zustellung ist ein POST mit JSON-Body und drei Headern:
| Header | Wert |
|---|---|
content-type | application/json |
x-readsira-event | Der Ereignisname, zum Beispiel bookmark.created |
x-readsira-signature | sha256= gefolgt vom HMAC des Bodys in Hex |
Der Body ist ein Umschlag:
{
"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 ist der Zeitpunkt, an dem Readsira die Zustellung gebaut hat. Die Werte oben sind ein
Beispiel.
Ereignisse
Du wählst die Ereignisse für jeden Webhook. Ein neuer Webhook bekommt bookmark.created.
| Ereignis | Gesendet, wenn | data |
|---|---|---|
bookmark.created | Du ein Lesezeichen speicherst | bookmark_id, user_id, url, title, tags, collection_id |
bookmark.updated | Du ein Lesezeichen änderst | bookmarkId, userId, bookmark, changes, notesChanged |
bookmark.deleted | Ein Lesezeichen gelöscht wird | bookmarkId, userId, bookmark |
bookmark.visited | Du ein Lesezeichen öffnest | bookmarkId, userId, visitedAt |
output.created | Ein Projekt-Ergebnis fertig wird | id, projectId, kind, title, version, sourceCount, completedAt |
Die App listet außerdem collection.created, collection.updated und collection.deleted. Readsira
sendet diese noch nicht, ein Webhook, der nur auf sie hört, erhält also nichts.
In bookmark.updated und bookmark.deleted ist bookmark ein Schnappschuss mit id, url, title,
description, collectionId, tags, isStarred, isPublic, domain, notes, triageStatus und
captureIntent. changes listet die Namen der Felder auf, die die Anfrage gesetzt hat. Bei
output.created enthält die Payload nur IDs und Metadaten, nie den Text des Ergebnisses; lies ihn über
die App.
Die Schaltfläche Testen in der App sendet ein weiteres Ereignis, webhook.test, mit
data: { "message": "Test webhook from Readsira", "userId": "..." }. Es ist wie die anderen signiert.
Webhooks pausieren, solange dein Konto zur Löschung vorgemerkt ist.
Die Signatur prüfen
Die Signatur beweist, dass die Anfrage von Readsira kam und niemand den Body verändert hat. Readsira
berechnet HMAC-SHA256 über den rohen Body, mit dem Geheimnis des Webhooks als Schlüssel, und sendet
das Ergebnis als sha256= plus 64 Hex-Zeichen.
Das Geheimnis ist der 64 Zeichen lange Hex-String, den die App anzeigt, wenn du den Webhook erstellst oder sein Geheimnis rotierst. Nutze diesen String so, wie er ist, als Text. Dekodiere ihn nicht aus Hex. Er wird einmal angezeigt.
Ein Empfänger in Node, ohne Pakete:
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);
Drei Details, die die meisten Fehler verursachen:
- Hashe die rohen Bytes. Wenn dein Framework das JSON zuerst parst und du es erneut serialisierst,
unterscheiden sich die Bytes, und die Prüfung schlägt fehl. Nutze in Express
express.raw({ type: 'application/json' })für die Webhook-Route. - Vergleiche in konstanter Zeit, wie es
timingSafeEqualtut, nicht mit===. - Nachdem du das Geheimnis in der App rotiert hast, bestehen Anfragen, die mit dem alten Geheimnis
signiert wurden, die Prüfung nicht. Aktualisiere zuerst
READSIRA_WEBHOOK_SECRET.
Um Replays abzulehnen, vergleiche timestamp mit der Uhr und verwirf Zustellungen, die älter als ein
paar Minuten sind. Der Zeitstempel steht im signierten Body, kann also nicht geändert werden, ohne die
Signatur zu brechen. Eine Wiederholung (unten) trägt das ursprüngliche Ereignis erneut, mache deinen
Handler also so, dass er zweimal laufen darf.
Schnell antworten
Antworte mit einem beliebigen 2xx-Status, sobald du das Ereignis gespeichert hast. Readsira wartet
5 Sekunden und liest höchstens 64 KB deiner Antwort.
| Deine Antwort | Was Readsira tut |
|---|---|
2xx | Die Zustellung gilt als zugestellt. |
5xx oder keine Antwort innerhalb von 5 Sekunden | Die Zustellung wird erneut versucht, insgesamt bis zu 4 Versuche, mit einer Wartezeit dazwischen, die bei 30 Sekunden beginnt und sich verdoppelt. |
4xx | Die Zustellung gilt als fehlgeschlagen und wird nicht erneut versucht. |
Die App zeigt für jeden Webhook, wie oft er ausgelöst wurde und wann zuletzt.
Tipps
- Halte den Handler kurz. Lege die Arbeit in eine Warteschlange und antworte zuerst.
- Nutze die Schaltfläche Testen, während du baust, bevor du ein echtes Lesezeichen speicherst.
- Bewahre das Geheimnis wie ein Passwort auf. Rotiere es, wenn es durchsickert.
Verwandte Themen
- Webhooks (Einrichtung in der App)
- Beispiele
- Ratenlimits und Fehler