Intégration
Webhooks
Événements, signature, déduplication et nouvelles tentatives.
Événements
license.created, license.activated, license.expired, license.renewed, license.revoked, license.suspended, license.device_reset, user.created, user.banned, application.disabled, et webhook.test depuis le dashboard.
json
{
"id": "1f0c…",
"type": "license.revoked",
"created_at": "2026-10-22T10:00:00.000Z",
"organization_id": "…",
"application_id": "…",
"data": { "license": { "id": "…", "key_preview": "LIC-••••-••••-••••-6A12", "status": "revoked", "expires_at": null, "max_devices": 1 }, "reason": "remboursement" }
}En-têtes
| En-tête | Contenu |
|---|---|
Keyline-Event-Id | Identifiant unique : clé de déduplication |
Keyline-Event-Type | Type d’événement |
Keyline-Delivery-Id | Identifiant de la livraison |
Keyline-Timestamp | Secondes unix de la tentative |
Keyline-Signature | v1=hex(HMAC-SHA256(secret, "ts.corps")), deux valeurs pendant une rotation |
Réception (Express)
typescript
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
const app = express();
const seen = new Set<string>(); // en production : table avec contrainte d'unicité
// Corps brut obligatoire : la signature porte sur les octets exacts.
app.post('/keyline/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const ts = Number(req.header('Keyline-Timestamp'));
const body = req.body.toString('utf8');
const expected = createHmac('sha256', process.env.KEYLINE_WEBHOOK_SECRET!).update(`${ts}.${body}`).digest();
const valid = Math.abs(Date.now() / 1000 - ts) <= 300 &&
String(req.header('Keyline-Signature')).split(',').some((part) => {
const sig = Buffer.from(part.trim().replace(/^v1=/, ''), 'hex');
return sig.length === expected.length && timingSafeEqual(sig, expected);
});
if (!valid) return res.status(401).end();
const eventId = req.header('Keyline-Event-Id')!;
if (seen.has(eventId)) return res.status(200).end(); // déjà traité (retry)
seen.add(eventId);
const event = JSON.parse(body);
switch (event.type) {
case 'license.revoked': /* … */ break;
case 'user.banned': /* … */ break;
}
res.status(200).end(); // répondre en moins de 10 s ; traiter en asynchrone si besoin
});Nouvelles tentatives
10 tentatives au maximum, délai 30 s × 2^(n-1) (±20 %, plafond 6 h). Réponse 2xx attendue en moins de 10 s ; les redirections ne sont pas suivies. Après 25 livraisons définitivement en échec consécutives, le webhook est désactivé. Les destinations privées, loopback ou internes sont refusées.