Aller au contenu
QasqadAPI

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énementQuand
batch.processedun lot est traité sans erreur : conducteurs identifiés, justificatifs produits
batch.requires_inputun 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êteContenu
X-Webhook-Eventle nom de l'événement
X-Webhook-Timestampl'heure d'envoi, en millisecondes Unix
X-Webhook-Signaturela signature, calculée comme pour vos appels, avec le secret du webhook à la place de la clé
batch.requires_input
{
  "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. Un 201, un 204 ou 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
});

Sur cette page