Webhooks
Laissez Qasqad prévenir votre SI dès qu'un lot est traité ou attend votre intervention.
Les activer
Dans Paramètres › Espace développeur, déclarez une URL HTTPS et les événements à écouter. Chaque webhook a son propre secret de signature (wh_sec_…), distinct de votre clé API.
| Événement | Quand |
|---|---|
batch.processed | un lot est traité sans erreur : conducteurs identifiés, justificatifs produits |
batch.requires_input | un FPS du lot au moins demande votre intervention ; manualInputReason dit lequel et pourquoi |
Une fois corrigé, un lot passé par batch.requires_input peut envoyer batch.processed.
Ce que vous recevez
Un POST JSON, avec trois en-têtes :
| En-tête | Contenu |
|---|---|
X-Webhook-Event | le nom de l'événement |
X-Webhook-Timestamp | l'heure d'envoi, en millisecondes Unix |
X-Webhook-Signature | la signature, calculée comme pour vos appels, avec le secret du webhook à la place de la clé |
{
"eventType": "batch.requires_input",
"data": {
"uuid": "9f1c2e4a-…",
"status": "has_error",
"type": "identify",
"isFromAntaiApi": true,
"processedAt": null,
"parkingTickets": [
{
"uuid": "c03b7d18-…",
"paymentNotice": "12345678901234567890",
"licencePlate": "AB-123-CD",
"offenceDate": "2026-10-28T14:05:00.000Z",
"amount": 35,
"collectivityName": "Ville de Lyon",
"manualInputReason": "driver_not_found",
"paymentNoticePath": null
}
]
}
}paymentNoticePath est un lien vers le justificatif de traitement, valable 6 jours ; malgré son nom, ce n'est pas l'avis.
Ce que vous devez répondre
- Un
200, exactement, sous 15 secondes. Un201, un204ou un délai dépassé comptent comme un échec : mettez le traitement en file et répondez tout de suite. - En cas d'échec, Qasqad réessaie : 4 tentatives en tout, espacées de 30 secondes, 1 minute puis 2 minutes.
- Un même événement peut donc arriver deux fois : dédoublonnez sur
eventType+data.uuid.
Vérifier la signature
Toujours, sur le corps brut reçu, avant de le parser ; et refusez un horodatage de plus de 5 minutes.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.QASQAD_WEBHOOK_SECRET; // wh_sec_…
app.post('/hooks/qasqad', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('X-Webhook-Timestamp') ?? '';
const signature = req.get('X-Webhook-Signature') ?? '';
const bodyHash = crypto.createHash('sha256').update(req.body).digest('hex');
const expected = crypto
.createHmac('sha256', SECRET)
.update(`${timestamp}.${bodyHash}`)
.digest('hex');
const valid =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)) &&
Math.abs(Date.now() - Number(timestamp)) < 5 * 60 * 1000;
if (!valid) return res.sendStatus(401);
const { eventType, data } = JSON.parse(req.body);
queue.add({ eventType, batchUuid: data.uuid }); // traiter plus tard, répondre vite
res.sendStatus(200); // 200 exactement
});