Recevez des notifications push en temps réel quand quelque chose se passe dans votre compte Quotery. Pas de polling, pas de cron jobs. Juste des callbacks HTTP signés livrés directement à votre serveur.
Un webhook est une notification push que Quotery envoie à votre serveur chaque fois que quelque chose d'important se produit. Un devis est accepté, un bon de livraison est marqué comme livré ou un bon de retour est créé. Au lieu de demander "du nouveau?" encore et encore (du polling, un gaspillage), nous vous prévenons au moment même où l'événement se déclenche.
Vous nous indiquez une URL et les types d'événement qui vous intéressent. Quand l'un de ces événements se produit dans votre compte, nous envoyons un payload JSON signé via POST à votre URL. Vous vérifiez la signature pour confirmer qu'il vient bien de nous, traitez les données et renvoyez un 200. C'est tout.
Les webhooks sont l'épine dorsale des intégrations en temps réel. Utilisez-les pour envoyer des commandes vers votre ERP, synchroniser le stock avec un système externe, déclencher des notifications Slack quand un devis se conclut ou construire un journal d'audit de tout ce qui se passe dans votre tenant. Vous choisissez les événements, vous gérez le récepteur.
Chaque type d'événement représente une action métier spécifique au sein de Quotery. Vous vous abonnez à ceux qui sont pertinents pour votre intégration. Choisissez-en quelques-uns ou les sept.
quote.closed — Se déclenche quand un devis est clôturé (finalisé et envoyé au client). Le payload inclut les détails du devis, les lignes et les totaux au moment de la clôture.quote.accepted — Se déclenche quand un client accepte un devis via le portail. C'est l'événement principal pour lancer l'expédition: créez une commande dans l'ERP, notifiez l'entrepôt ou générez une facture.quote.cancelled — Se déclenche quand un devis est annulé. Utilisez-le pour défaire les processus: libérer le stock réservé, annuler une commande en brouillon ou mettre à jour l'étape de l'affaire dans le CRM.delivery_note.created — Se déclenche quand un bon de livraison est créé. Si vous suivez les expéditions dans un système externe, cet événement vous signale qu'une nouvelle livraison est en préparation.delivery_note.marked_delivered — Se déclenche quand un bon de livraison est marqué comme livré. C'est la confirmation que les marchandises sont arrivées. Utilisez-le pour décrémenter le stock, clôturer une tâche d'expédition ou déclencher une facture.stock_receipt.completed — Se déclenche quand un bon de réception est finalisé. Les marchandises reçues sont désormais dans votre stock. Synchronisez avec votre WMS ou mettez à jour les niveaux de stock dans un catalogue externe.return_note.created — Se déclenche quand un bon de retour est créé. Un client renvoie quelque chose. Vous voudrez ajuster le stock, lancer un flux de remboursement ou enregistrer le retour dans votre système de commandes.Configurer un webhook prend environ une minute. Vous avez besoin d'un accès admin à votre compte Quotery. La gestion des webhooks est une fonction administrative.
Le secret de signature est généré automatiquement quand vous créez le webhook. Vous ne le choisissez pas, vous le recevez. Et vous ne le voyez qu'une seule fois, juste après la création. Copiez-le dans un endroit sûr avant de fermer cet onglet de réponse.
Vous devez être connecté en tant qu'admin pour créer ou gérer des webhooks. Si vous n'êtes pas admin, adressez-vous à quelqu'un de votre équipe qui l'est.
Chaque livraison de webhook inclut une signature cryptographique pour que vous puissiez confirmer que le payload vient de Quotery et n'a pas été altéré. La vérification est obligatoire. Ne traitez jamais un payload de webhook sans d'abord vérifier la signature.
Nous signons le corps brut de la requête avec HMAC-SHA256 en utilisant le secret de signature unique de votre webhook. La signature est envoyée dans l'en-tête X-Quotery-Signature dans un format compatible avec le schéma de Stripe. Si vous avez déjà travaillé avec les webhooks Stripe, cela vous semblera familier.
L'en-tête X-Quotery-Signature ressemble à ceci:
t=1714789200,v1=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2import hmac, hashlib, time
def verify_signature(body: bytes, signature_header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
timestamp = int(parts["t"])
if abs(time.time() - timestamp) > tolerance:
return False # outside tolerance window
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(parts["v1"], expected)Le paramètre de tolérance (300 secondes par défaut = 5 minutes) rejette les livraisons trop anciennes. C'est votre protection contre les attaques par rejeu. Ajustez-le si vos horloges sont désynchronisées, mais ne le définissez pas au-delà de 5 minutes.
const crypto = require("crypto");
function verifySignature(rawBody, signatureHeader, secret, tolerance = 300) {
// Parse "t=1714789200,v1=abc123..." into { t, v1 }
const parts = {};
signatureHeader.split(",").forEach(p => {
const idx = p.indexOf("=");
parts[p.slice(0, idx)] = p.slice(idx + 1);
});
const timestamp = parseInt(parts.t, 10);
const now = Math.floor(Date.now() / 1000);
// Reject if outside tolerance window (5 minutes)
if (Math.abs(now - timestamp) > tolerance) {
return false;
}
// Compute the expected signature
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
// Constant-time comparison prevents timing attacks
return crypto.timingSafeEqual(
Buffer.from(parts.v1),
Buffer.from(expected)
);
}La vérification de signature utilise timingSafeEqual au lieu d'une comparaison de chaînes classique. C'est important. Les opérateurs d'égalité classiques s'arrêtent au premier écart, ce qui divulgue des informations temporelles sur la partie de la signature qui correspondait. Utilisez toujours une comparaison en temps constant pour la vérification HMAC.
Chaque livraison POST inclut ces quatre en-têtes. Vous les utilisez pour la vérification de signature, l'idempotence et les décisions de routage.
| En-tête | Valeur |
|---|---|
| Content-Type | application/json |
| X-Quotery-Webhook-Id | UUID (ex. 550e8400-e29b-41d4-a716-446655440000) |
| X-Quotery-Event | Type d'événement séparé par un point (ex. quote.closed) |
| X-Quotery-Signature | t=1714789200,v1=abc123... |
Content-Type — Le corps de la requête est toujours du JSON. Votre endpoint peut le supposer et effectuer le parsing.
X-Quotery-Webhook-Id — Un identifiant unique pour cette tentative de livraison. Utilisez-le comme clé d'idempotence. Si une livraison est renvoyée, elle porte le même ID. Vous dédupliquez en vérifiant si vous avez déjà traité cet ID.
X-Quotery-Event — Vous indique exactement quel événement s'est déclenché. Utilisez-le dans un switch pour router les différents événements vers différents gestionnaires.
X-Quotery-Signature — La signature HMAC-SHA256. Effectuez le parsing, vérifiez-la et ne traitez le payload que si elle est valide.
Si votre endpoint est hors service ou renvoie un statut non-2xx, Quotery renvoie la livraison automatiquement. Vous n'avez rien à configurer. Le calendrier de nouvelles tentatives est intégré.
Les livraisons en échec sont renvoyées jusqu'à 6 fois avec un backoff exponentiel. Voici la chronologie à partir de la tentative initiale:
La fenêtre totale de nouvelles tentatives couvre environ 12 heures. Après 6 tentatives en échec, la livraison est abandonnée définitivement.
Chaque webhook suit deux indicateurs: le timestamp de la dernière livraison réussie et un compteur d'échecs consécutifs. Une livraison réussie remet le compteur à zéro. Une série d'échecs l'incrémente. Vous vérifiez les deux à tout moment via l'API. Pas de logs à éplucher, pas d'alertes à configurer. Si le compteur d'échecs augmente, votre endpoint est probablement hors service ou rejette les livraisons.
Comme les nouvelles tentatives réutilisent le même X-Quotery-Webhook-Id, votre endpoint peut gérer sans risque les livraisons dupliquées. Conservez un petit cache des ID de webhook traités récemment (quelques heures suffisent, vu la fenêtre de 12 heures) et ignorez toute livraison dont vous avez déjà vu l'ID.
Chaque livraison de webhook porte la même enveloppe externe. Les détails spécifiques à l'événement se trouvent dans l'objet data.
{
"event_type": "quote.closed",
"tenant_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": "2026-05-03T10:00:00Z",
"data": {
// Payload spécifique à l'événement — varie selon event_type
}
}Vous n'avez pas besoin d'un environnement de staging pour tester les webhooks. Vous pouvez déclencher des événements réels dans votre compte Quotery et les voir arriver sur votre endpoint en temps réel.
Prêt à vous lancer? Voici les deux tâches les plus courantes (créer un webhook et recevoir des livraisons) en format prêt à copier-coller.
Cette commande curl crée un webhook qui écoute les événements quote.accepted, quote.closed et delivery_note.marked_delivered. Vous avez besoin du cookie de session d'une session de navigateur admin authentifiée.
curl -X POST https://app.quotery.io/api/v1/webhooks/ \
-H "Content-Type: application/json" \
-H "Cookie: sessionid=<votre-id-de-session-admin>" \
-d '{
"name": "Mon Intégration",
"url": "https://monapp.exemple.com/webhooks/quotery",
"events": ["quote.accepted", "quote.closed", "delivery_note.marked_delivered"]
}'La réponse 201 inclut l'ID du webhook et, point critique, le signing_secret. Copiez ce secret maintenant. Vous ne le reverrez plus.
Voici un endpoint Express minimal qui vérifie la signature, contrôle la tolérance de timestamp et déduplique par ID de webhook. Intégrez-le dans votre app Node existante ou utilisez-le comme point de départ.
const express = require("express");
const crypto = require("crypto");
const app = express();
const SECRET = process.env.QUOTERY_WEBHOOK_SECRET;
const TOLERANCE = 300; // 5 minutes en secondes
// Stockage d'idempotence en mémoire — utilisez Redis ou une BD en production
const processedIds = new Set();
app.post("/webhooks/quotery", express.json({
verify: (req, _res, buf) => { req.rawBody = buf; }
}), (req, res) => {
// 1. Effectuer le parsing de l'en-tête de signature
const header = req.headers["x-quotery-signature"];
if (!header) return res.status(400).send("Signature absente");
const parts = {};
header.split(",").forEach(p => {
const idx = p.indexOf("=");
parts[p.slice(0, idx)] = p.slice(idx + 1);
});
// 2. Rejeter si le timestamp est hors tolérance
const timestamp = parseInt(parts.t, 10);
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > TOLERANCE) {
return res.status(400).send("Timestamp hors tolérance");
}
// 3. Vérifier la signature HMAC
const expected = crypto
.createHmac("sha256", SECRET)
.update(req.rawBody)
.digest("hex");
const valid = crypto.timingSafeEqual(
Buffer.from(parts.v1),
Buffer.from(expected)
);
if (!valid) return res.status(400).send("Signature invalide");
// 4. Dédupliquer par ID de webhook
const deliveryId = req.headers["x-quotery-webhook-id"];
if (processedIds.has(deliveryId)) {
return res.sendStatus(200); // Déjà traité — confirmer silencieusement
}
processedIds.add(deliveryId);
// 5. Traiter l'événement
const eventType = req.headers["x-quotery-event"];
const payload = req.body;
console.log(`Reçu ${eventType}`, payload);
switch (eventType) {
case "quote.accepted":
// Créer une commande dans votre ERP
break;
case "quote.closed":
// Mettre à jour l'étape de l'affaire dans votre CRM
break;
case "delivery_note.marked_delivered":
// Décrémenter le stock, déclencher la facture
break;
default:
console.log(`Type d'événement non traité: ${eventType}`);
}
res.sendStatus(200);
});
app.listen(process.env.PORT || 3000, () => {
console.log("Récepteur de webhook à l'écoute");
});La vérification d'idempotence à l'étape 4 est importante. Quotery renvoie les livraisons jusqu'à 6 fois et chaque nouvelle tentative porte le même X-Quotery-Webhook-Id. Sans déduplication, vous traiteriez le même événement plusieurs fois: commandes dupliquées, stocks comptés en double, et ainsi de suite.