Aller au contenu principal

Webhooks pour les développeurs

Disponible avecFreeProResearch

Un webhook fait envoyer à Readsira une requête HTTP POST à votre serveur quand quelque chose se produit dans votre compte, par exemple quand vous enregistrez un marque-page. Cette page s'adresse au code qui le reçoit : les événements, la charge utile, la signature et les règles de relance. Pour ajouter un webhook dans l'application, voir Webhooks.

Avant de commencer​

  • Une adresse http ou https publique qui accepte POST. Readsira refuse localhost et les adresses de réseau privé, et ne suit pas les redirections.
  • Votre serveur doit lire le corps brut de la requête. Il vous faut les octets exacts pour vérifier la signature.

À quoi ressemble une livraison​

Chaque livraison est un POST avec un corps JSON et trois en-têtes :

En-têteValeur
content-typeapplication/json
x-readsira-eventLe nom de l'événement, par exemple bookmark.created
x-readsira-signaturesha256= suivi du HMAC du corps, en hexadécimal

Le corps est une enveloppe :

{
"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 est le moment où Readsira a construit la livraison. Les valeurs ci-dessus sont un exemple.

Événements​

Vous choisissez les événements de chaque webhook. Un nouveau webhook reçoit bookmark.created.

ÉvénementEnvoyé quanddata
bookmark.createdVous enregistrez un marque-pagebookmark_id, user_id, url, title, tags, collection_id
bookmark.updatedVous modifiez un marque-pagebookmarkId, userId, bookmark, changes, notesChanged
bookmark.deletedUn marque-page est supprimébookmarkId, userId, bookmark
bookmark.visitedVous ouvrez un marque-pagebookmarkId, userId, visitedAt
output.createdUne production de projet se termineid, projectId, kind, title, version, sourceCount, completedAt

L'application liste aussi collection.created, collection.updated et collection.deleted. Readsira ne les envoie pas encore : un webhook qui n'écoute que ceux-là ne reçoit rien.

Dans bookmark.updated et bookmark.deleted, bookmark est un instantané avec id, url, title, description, collectionId, tags, isStarred, isPublic, domain, notes, triageStatus et captureIntent. changes liste les noms des champs définis par la requête. Pour output.created, la charge utile ne contient que des identifiants et des métadonnées, jamais le texte de la production ; lisez-le dans l'application.

Le bouton Tester de l'application envoie un événement de plus, webhook.test, avec data: { "message": "Test webhook from Readsira", "userId": "..." }. Il est signé comme les autres.

Les webhooks sont suspendus tant que la suppression de votre compte est programmée.

Vérifier la signature​

La signature prouve que la requête vient de Readsira et que personne n'a modifié le corps. Readsira calcule un HMAC-SHA256 sur le corps brut, avec le secret du webhook comme clé, et envoie le résultat sous la forme sha256= suivi de 64 caractères hexadécimaux.

Le secret est la chaîne hexadécimale de 64 caractères que l'application affiche quand vous créez le webhook ou renouvelez son secret. Utilisez cette chaîne telle quelle, comme du texte. Ne la décodez pas depuis l'hexadécimal. Elle n'est affichée qu'une seule fois.

Un récepteur en Node, sans aucun paquet :

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);

Trois détails causent la plupart des échecs :

  • Hachez les octets bruts. Si votre framework analyse d'abord le JSON et que vous le sérialisez de nouveau, les octets diffèrent et la vérification échoue. Avec Express, utilisez express.raw({ type: 'application/json' }) pour la route du webhook.
  • Comparez en temps constant, comme le fait timingSafeEqual, et non avec ===.
  • Après avoir renouvelé le secret dans l'application, les requêtes signées avec l'ancien secret échouent à la vérification. Mettez d'abord à jour READSIRA_WEBHOOK_SECRET.

Pour rejeter les rejeux, comparez timestamp à l'horloge et écartez les livraisons plus anciennes que quelques minutes. L'horodatage se trouve dans le corps signé : il ne peut donc pas être modifié sans casser la signature. Une relance (ci-dessous) transporte de nouveau l'événement d'origine ; rendez donc votre gestionnaire sûr à exécuter deux fois.

Répondre rapidement​

Répondez avec n'importe quel statut 2xx dès que vous avez stocké l'événement. Readsira attend 5 secondes et lit au plus 64 Ko de votre réponse.

Votre réponseCe que fait Readsira
2xxLa livraison est comptée comme livrée.
5xx ou aucune réponse en 5 secondesLa livraison est retentée, jusqu'à 4 tentatives en tout, avec une attente entre elles qui commence à 30 secondes et double à chaque fois.
4xxLa livraison est comptée comme échouée et n'est pas retentée.

L'application montre, pour chaque webhook, combien de fois il s'est déclenché et quand pour la dernière fois.

Conseils​

  • Gardez le gestionnaire court. Mettez le travail dans une file et répondez d'abord.
  • Utilisez le bouton Tester pendant le développement, avant d'enregistrer un vrai marque-page.
  • Stockez le secret comme un mot de passe. Renouvelez-le s'il fuit.