Webhooks & API produits / stock
Dernière mise à jour : 8 septembre 2026
YeloInvoice expose votre catalogue et votre stock à vos autres systèmes — boutique en ligne, ERP, marketplace, entrepôt. Deux mécanismes complémentaires : des webhooks signés qui vous préviennent en temps réel, et une API de lecture versionnée pour l'amorçage initial et la réconciliation. Un intégrateur autorisé peut aussi écrire des mouvements de stock.
1. Vue d'ensemble
Chaque modification de produit ou de stock écrit un événement dans une file d'attente, dans la même transaction que la donnée. Un rollback n'émet rien, un commit n'oublie rien. Un worker envoie ensuite l'événement en POST signé à vos endpoints, avec réessais.
- Push — webhooks
product.created,product.updated,product.deleted,stock.updated. - Pull —
GET /v1/integration/products,/stock,/events. - Écriture —
POST /v1/integration/stock/movementset/stock/counts, réservés aux clés portantstock:write.
Le stock n'est jamais « remplacé » : YeloInvoice tient un journal de mouvements en ajout seul. Un système tiers y ajoute un mouvement signé, exactement comme le fait une facture — les deux parties écrivent dans le même journal et aucune n'écrase l'autre.
2. Obtenir une clé API
Dans YeloInvoice, ouvrez Paramètres → API & Webhooks (permission integration:manage), onglet Clés API. Choisissez un nom et les portées nécessaires :
products:read— lire les fiches produits.stock:read— lire les quantités.events:read— rejouer les événements manqués.stock:write— écrire des mouvements de stock.
Le jeton complet (yi_live_…) n'est affiché qu'une seule fois, à la création. Stockez-le dans le gestionnaire de secrets du système appelant ; s'il est perdu, révoquez la clé et créez-en une nouvelle.
3. Authentifier l'API de lecture
Toutes les routes attendent un en-tête Authorization: Bearer. Vérifiez votre installation avec /ping, qui renvoie l'identité de la clé, la société rattachée et la dernière séquence connue :
curl -H "Authorization: Bearer yi_live_..." \
https://api.yeloinvoice.com/v1/integration/ping{
"ok": true,
"apiVersion": "2026-09-01",
"apiKey": { "id": "ak_...", "name": "Boutique", "scopes": ["products:read"] },
"company": { "id": "cmp_...", "name": "...", "currency": "TND" },
"latestSequence": 5312,
"eventRetentionDays": 30
}Les listes se paginent par curseur opaque : ?limit= (200 maximum, 50 par défaut) et ?cursor=. La réponse porte { "data": [...], "nextCursor": "…" | null }. Ajoutez ?updatedSince= (date ISO 8601) pour ne relire que ce qui a bougé, et ?includeInactive=true pour voir aussi les produits désactivés — indispensable lors d'une resynchronisation complète, pour dépublier ce qui ne doit plus être vendu.
4. Créer un endpoint webhook
Toujours dans Paramètres → API & Webhooks, onglet Webhooks : renseignez une URL HTTPS publique et cochez les événements souscrits. Les adresses internes (localhost, plages privées, métadonnées cloud) sont refusées, à la création comme à chaque envoi.
Le secret de signature est généré à la création et affiché une fois ; il reste consultable via le bouton Révéler le secret. Le bouton Envoyer un test émet un événement webhook.test signé — idéal pour valider votre vérification de signature avant la mise en production.
Chaque livraison porte ces en-têtes :
content-type: application/json; charset=utf-8
user-agent: YeloFact-Webhooks/1.0
x-yelo-event: product.updated
x-yelo-event-id: evt_ckz...
x-yelo-sequence: 5312
x-yelo-delivery: dlv_ckz...
x-yelo-timestamp: 1789040400
x-yelo-signature: v1=8f4c...5. Vérifier la signature
La signature est un HMAC-SHA256 de {timestamp}.{corps brut}, calculé avec le secret de l'endpoint et préfixé v1=. Trois règles, toutes nécessaires :
- signez le corps brut, jamais un JSON re-sérialisé — un espace de différence invalide la signature ;
- comparez en temps constant ;
- rejetez un
x-yelo-timestampvieux de plus de 300 secondes (fenêtre anti-rejeu).
const crypto = require("crypto");
// Le corps BRUT est indispensable : express.json() le détruit.
app.post(
"/webhooks/yelo",
express.raw({ type: "application/json" }),
(req, res) => {
const timestamp = Number(req.get("x-yelo-timestamp"));
const received = req.get("x-yelo-signature") ?? "";
const raw = req.body.toString("utf8");
if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) {
return res.status(400).send("stale timestamp");
}
const expected =
"v1=" +
crypto
.createHmac("sha256", process.env.YELO_WEBHOOK_SECRET)
.update(timestamp + "." + raw, "utf8")
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(received);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send("bad signature");
}
const event = JSON.parse(raw);
// Répondez 2xx VITE, traitez ensuite : au-delà de 10 s, l'envoi est réessayé.
res.status(200).send("ok");
void handle(event);
},
);<?php
$raw = file_get_contents("php://input");
$timestamp = (int) ($_SERVER["HTTP_X_YELO_TIMESTAMP"] ?? 0);
$received = $_SERVER["HTTP_X_YELO_SIGNATURE"] ?? "";
if ($timestamp === 0 || abs(time() - $timestamp) > 300) {
http_response_code(400);
exit("stale timestamp");
}
$expected = "v1=" . hash_hmac("sha256", $timestamp . "." . $raw, getenv("YELO_WEBHOOK_SECRET"));
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit("bad signature");
}
$event = json_decode($raw, true);
http_response_code(200);
echo "ok";6. Référence des événements
Toute livraison — et toute entrée de l'API de rejeu — partage la même enveloppe :
{
"id": "evt_ckz...",
"sequence": 5312,
"type": "product.updated",
"apiVersion": "2026-09-01",
"createdAt": "2026-09-08T09:15:04.221Z",
"companyId": "cmp_...",
"origin": { "type": "user", "id": "usr_...", "name": "Amine" },
"data": { }
}sequence est un compteur strictement croissant par société : persistez la dernière valeur appliquée et ignorez tout événement dont la séquence lui est inférieure ou égale — c'est ce qui rend votre consommateur idempotent. origin identifie l'auteur de la mutation (user, api_key, system) et vous permet d'ignorer l'écho de vos propres écritures.
product.created · product.updated
data porte l'instantané public complet du produit. Les montants sont en millimes (entiers, jamais de flottant) et les quantités en millièmes d'unité. product.updated n'est émis que si l'instantané public a réellement changé — une modification purement interne ne vous réveille pas, et un mouvement de stock émet stock.updated, pas product.updated.
{
"id": "prd_ckz...",
"sku": "IPH-15-128-NOIR",
"barcode": "6194000123456",
"articleCode": "A-1042",
"name": "iPhone 15 128 Go Noir",
"description": null,
"brand": "Apple",
"family": "Smartphones",
"category": { "id": "cat_...", "name": "Téléphonie" },
"unit": "pièce",
"isService": false,
"active": true,
"priceHT": { "amountMillimes": 3190000, "currency": "TND" },
"vatRateBasisPoints": 1900,
"priceTTC": { "amountMillimes": 3796100, "currency": "TND" },
"stock": {
"totalQuantityMilli": 7000,
"byWarehouse": [
{ "warehouseId": "wh_...", "name": "Dépôt principal", "quantityMilli": 7000 }
]
},
"createdAt": "2026-09-02T08:30:00.000Z",
"updatedAt": "2026-09-08T09:15:04.180Z"
}vatRateBasisPoints est en points de base : 1900 = 19 %. priceTTC est fourni pour éviter que chaque intégrateur réimplémente l'arrondi. createdAt est l'entrée du produit au catalogue Yelo — utile pour trier des nouveautés, là où votre propre date d'import est la même pour tout le lot.
product.deleted
{ "id": "prd_ckz...", "sku": "IPH-15-128-NOIR", "deletedAt": "2026-09-08T09:20:00.000Z" }stock.updated
Charge utile allégée : le stock bouge bien plus souvent que la fiche produit. Émis pour toute variation, quelle qu'en soit la source (facture, bon de livraison, caisse, réception, inventaire, transfert, écriture API).
{
"productId": "prd_ckz...",
"sku": "IPH-15-128-NOIR",
"stock": {
"totalQuantityMilli": 6000,
"byWarehouse": [
{ "warehouseId": "wh_...", "name": "Dépôt principal", "quantityMilli": 6000 }
]
},
"updatedAt": "2026-09-08T09:22:11.004Z"
}7. Réessais et réponses attendues
Répondez 2xx dès que l'événement est accepté, et traitez-le ensuite. Toute autre réponse — ou un dépassement du délai d'attente — programme un réessai selon ce barème :
10 s → 30 s → 2 min → 10 min → 1 h → 6 h (6 tentatives, ~8 h)- 2xx — livré, aucune nouvelle tentative.
- 4xx / 5xx / timeout — réessai jusqu'à épuisement du barème.
- Après 20 livraisons épuisées consécutives, l'endpoint est désactivé automatiquement et le motif s'affiche dans l'interface — corrigez, réactivez, puis redélivrez.
Les redirections ne sont pas suivies : donnez l'URL finale. L'onglet Livraisons conserve le corps exact transmis, le code HTTP, la durée et l'erreur, et permet de rejouer une livraison une fois le consommateur réparé.
8. Amorçage et réconciliation
Amorçage — parcourez GET /v1/integration/products par curseur jusqu'à nextCursor: null, puis mémorisez la latestSequence renvoyée par /ping.
Rattrapage — après une panne ou un déploiement, rejouez ce que vous avez manqué plutôt que de tout resynchroniser :
curl -H "Authorization: Bearer yi_live_..." \
"https://api.yeloinvoice.com/v1/integration/events?afterSequence=5312&limit=100"
{ "data": [ /* enveloppes */ ], "lastSequence": 5412, "hasMore": true }Bouclez tant que hasMore vaut true, en repartant de lastSequence. Filtrez au besoin avec ?types=product.updated,stock.updated.
Les événements sont conservés un temps limité (voir eventRetentionDays dans /ping). Si votre afterSequence est plus ancienne que la rétention, l'API répond 410 Gone — la seule réponse correcte est alors une resynchronisation complète :
410 Gone
{
"error": "sequence_too_old",
"resyncRequired": true,
"oldestSequence": 5120
}9. Écrire des mouvements de stock
Avec la portée stock:write, votre système ajoute des mouvements signés. Chaque appel porte une référence externe { type, id } qui rend l'opération idempotente : rejouer la même référence ne double jamais le mouvement (la réponse porte alors "duplicate": true).
{
"reference": { "type": "HIGHTECH_ORDER", "id": "SO-2026-0042" },
"reason": "DELIVERY",
"lines": [
{ "sku": "IPH-15-128-NOIR", "quantityMilli": -1000 }
]
}quantityMilli est signé : négatif pour une sortie, positif pour une entrée, en millièmes d'unité (-1000 = une pièce sortie). Chaque ligne identifie son produit par sku ou productId, jamais les deux. Les motifs acceptés sont DELIVERY, RECEIPT, ADJUSTMENT et INVENTORY.
Pour un comptage absolu, utilisez POST /v1/integration/stock/counts : YeloInvoice calcule le delta et poste un mouvement d'inventaire. Ajoutez expectedQuantityMilli pour un verrou optimiste — si la quantité a bougé entre temps, la ligne est refusée plutôt qu'appliquée à l'aveugle.
Sauf autorisation explicite sur la clé, un mouvement qui ferait passer un stock sous zéro est refusé.
10. Erreurs et limites
- 400 — corps invalide ; la réponse détaille le champ fautif.
- 401 — clé absente, inconnue, révoquée ou expirée.
- 403 — portée manquante ; le corps nomme la portée requise.
- 404 — ressource inexistante dans la société rattachée à la clé.
- 409 — conflit de verrou optimiste sur un comptage.
- 410 — séquence trop ancienne, resynchronisation requise.
- 429 — au-delà de 600 requêtes par minute et par clé. Réessayez avec un back-off exponentiel.
Une clé n'accède qu'à la société pour laquelle elle a été créée : aucune valeur transmise par l'appelant ne peut élargir ce périmètre. La version du contrat (apiVersion) accompagne chaque réponse ; les évolutions sont additives, et un changement incompatible passerait par une nouvelle version.
Une question d'intégration ? Écrivez-nous.
