Doc développeur

Webhook de publication

Recevez les articles LinkQuiver en POST JSON signé HMAC sur votre propre endpoint. Le canal de publication pour les sites non-WordPress : Next.js, Symfony, Astro, CMS headless…

Comment ça marche

À chaque date planifiée, LinkQuiver envoie une requête POST application/json à l'URL que vous avez configurée. Le corps contient l'article complet (HTML + Markdown + image de couverture). Vérifiez la signature, répondez 2xx pour accuser réception, puis faites-en ce que vous voulez : publier, stocker, transformer.

Configuration

Dashboard → votre site → onglet Intégrations → section Webhook. Enregistrez votre URL : le secret de signature (64 caractères hexadécimaux) est affiché une seule fois à la création. Stockez-le côté serveur (variable d'environnement, jamais dans du code client). Secret perdu ? Régénérez-le depuis le même écran puis mettez à jour votre receveur.

En-têtes envoyés à chaque appel

  • X-LQ-Event type d'événement (ex. article.published)
  • X-LQ-Delivery-Id identifiant unique de la livraison (= idempotency_key, pour dédupliquer)
  • X-LQ-Timestamp horodatage ISO 8601 de l'envoi, à inclure dans le calcul HMAC
  • X-LQ-Signature sha256=<hex> : la signature HMAC à vérifier

Compatibilité : chaque requête porte aussi les mêmes valeurs sous les anciens noms X-BLE-* (X-BLE-Signature, X-BLE-Timestamp…). Les receveurs existants continuent de fonctionner ; utilisez X-LQ-* pour toute nouvelle intégration.

Vérifier la signature

Chaque requête est signée HMAC SHA-256 avec votre secret. La chaîne signée est la concaténation du timestamp, d'un point et du corps brut :

signature = "sha256=" + hex( HMAC_SHA256( secret, timestamp + "." + body ) )
  1. Lisez les en-têtes X-LQ-Signature et X-LQ-Timestamp.
  2. Concaténez timestamp + "." + corps BRUT de la requête (les octets reçus, pas un objet re-sérialisé).
  3. Calculez le HMAC SHA-256 avec votre secret, encodez en hexadécimal, préfixez de sha256=.
  4. Comparez avec X-LQ-Signature en temps constant (timingSafeEqual ou équivalent).
  5. Recommandé : rejetez si l'écart entre maintenant et timestamp dépasse 5 minutes (anti-rejeu).

Signez le corps BRUT reçu, jamais un objet re-sérialisé : JSON.stringify peut réordonner les clés et casser la signature.

Événements possibles

  • test envoyé par le bouton « Tester » du dashboard
  • article.published nouvel article publié (objet article)
  • article.link_inserted article existant mis à jour avec un lien inséré (objet article + link_insertion)
  • resurrection.published page d'un domaine ressuscité (objet page au lieu de article)

Exemple de payload (article.published)

{
  "event": "article.published",
  "delivery_id": "9f1c2e7a-4b3d-4f8a-9c10-2b6e5d8a1f33",
  "idempotency_key": "9f1c2e7a-4b3d-4f8a-9c10-2b6e5d8a1f33",
  "timestamp": "2026-06-26T10:00:00.000Z",
  "site": {
    "domain": "villaor.fr",
    "vdl_website_id": "b2a1c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
  },
  "article": {
    "id": "c7d8e9f0-1234-5678-9abc-def012345678",
    "subject": "Location villa avec piscine en Provence",
    "title": "Louer une villa avec piscine en Provence : le guide",
    "slug": "louer-villa-piscine-provence-guide",
    "html": "<h1>Louer une villa…</h1><p>…</p>",
    "markdown": "# Louer une villa…\n\n…",
    "meta_title": "Villa avec piscine en Provence | Guide",
    "meta_description": "Tout pour louer la bonne villa…",
    "cover_image_url": "https://…/cover.jpg",
    "scheduled_at": "2026-06-26T10:00:00.000Z"
  }
}

Images : valides 30 jours

Les images (la couverture cover_image_url ET les <img> dans le HTML/Markdown) ne sont garanties que 30 jours : passé ce délai, leurs URLs renvoient une 404. Téléchargez-les et réhébergez-les sur votre propre stockage dès réception.

Réponse attendue et bonnes pratiques

  • Répondez 2xx rapidementtoute autre réponse compte comme un échec ; le timeout est de 30 secondes. Traitez en asynchrone si besoin et accusez réception tout de suite.
  • Dédupliquez avec delivery_idune livraison échouée est retentée (jusqu'à 3 tentatives) : le même delivery_id peut donc arriver plusieurs fois.
  • Endpoint public obligatoireles adresses internes (localhost, IP privées) sont refusées. HTTPS fortement recommandé.
  • User-Agentles requêtes portent le User-Agent LinkQuiver-Webhook/1.0, utile si vous filtrez en amont.

Exemple complet : Node.js / Express

import crypto from 'node:crypto';
import express from 'express';

const SECRET = process.env.LQ_WEBHOOK_SECRET; // le secret affiché une seule fois
const app = express();
app.use(express.raw({ type: 'application/json' })); // IMPORTANT : on signe le corps BRUT, pas un objet re-sérialisé

app.post('/api/webhook', (req, res) => {
  const signature = req.header('X-LQ-Signature') ?? '';
  const timestamp = req.header('X-LQ-Timestamp') ?? '';
  const body = req.body.toString('utf8');

  const expected =
    'sha256=' +
    crypto.createHmac('sha256', SECRET)
          .update(`${timestamp}.${body}`)
          .digest('hex');

  const ok =
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  if (!ok) return res.status(401).json({ reason: 'invalid_signature' });

  // optionnel : rejeter si |maintenant - timestamp| > 5 min
  const payload = JSON.parse(body); // signature valide → traitez le payload
  console.log(payload.event, payload.article?.title);

  return res.status(200).json({ received: true });
});

Prêt à brancher votre site ?

Configurez votre webhook en deux minutes depuis les connexions de votre dashboard, puis validez avec l'événement test.

Configurer dans le dashboard