Zum Hauptinhalt springen

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- oder https-Adresse, die POST annimmt. Readsira lehnt localhost und 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:

HeaderWert
content-typeapplication/json
x-readsira-eventDer Ereignisname, zum Beispiel bookmark.created
x-readsira-signaturesha256= 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.

EreignisGesendet, wenndata
bookmark.createdDu ein Lesezeichen speicherstbookmark_id, user_id, url, title, tags, collection_id
bookmark.updatedDu ein Lesezeichen änderstbookmarkId, userId, bookmark, changes, notesChanged
bookmark.deletedEin Lesezeichen gelöscht wirdbookmarkId, userId, bookmark
bookmark.visitedDu ein Lesezeichen öffnestbookmarkId, userId, visitedAt
output.createdEin Projekt-Ergebnis fertig wirdid, 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 timingSafeEqual tut, 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 AntwortWas Readsira tut
2xxDie Zustellung gilt als zugestellt.
5xx oder keine Antwort innerhalb von 5 SekundenDie Zustellung wird erneut versucht, insgesamt bis zu 4 Versuche, mit einer Wartezeit dazwischen, die bei 30 Sekunden beginnt und sich verdoppelt.
4xxDie 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.