Documentation du webhook

Dernière mise à jour : 2026-10-14

Avec le forfait Pro ou Entreprise, AgentAO peut envoyer vos alertes d'appels d'offres à votre propre système : chaque résumé (le même que le courriel, à la même fréquence) devient une requête POST en JSON vers l'adresse que vous avez enregistrée dans Paramètres → Notifications.

La requête

POST /votre-adresse HTTP/1.1
Content-Type: application/json; charset=utf-8
User-Agent: AgentAO-Alerts/1.0 (+https://agentao.ca)
X-AgentAO-Event: alert.digest
X-AgentAO-Delivery: 5d0c6c3e-2f7a-4f43-9a51-0f1c1f0a7b21
X-AgentAO-Signature: t=1760479200,v1=8f3b…

{
  "event": "alert.digest",
  "delivery_id": "5d0c6c3e-2f7a-4f43-9a51-0f1c1f0a7b21",
  "sent_at": "2026-10-14T11:00:04.120513+00:00",
  "frequency": "daily",
  "locale": "fr",
  "tenders": [
    {
      "id": 48213,
      "title": "Réfection de pavage — rue Principale",
      "organization": "Ville de Laval",
      "region": "Laval",
      "sector": "Construction",
      "closing_date": "2026-11-01T14:00:00",
      "closing_date_timezone": "America/Toronto",
      "estimated_value": 250000.0,
      "url": "https://agentao.ca/dashboard/rfp/48213",
      "saved_search": "Pavage Laval"
    }
  ],
  "overflow": 0,
  "dashboard_url": "https://agentao.ca/dashboard"
}

tenders liste au plus 25 appels d'offres ; overflow compte ceux qui correspondent aussi mais ne sont pas listés (ils sont dans le tableau de bord). closing_date est l'heure du Québec, sans fuseau dans la valeur. estimated_value et region peuvent être nuls. saved_search est le nom de la recherche enregistrée qui a trouvé l'appel d'offres, ou nul quand c'est votre profil (secteurs, mots-clés).

Vérifier la signature

La clé de signature (whsec_…) est affichée une seule fois, à l'ajout du webhook ; le bouton « Nouvelle clé » en crée une autre et invalide l'ancienne. La signature est le HMAC-SHA256, en hexadécimal, de <t>.<corps brut>. Calculez-la sur les octets reçus, avant tout décodage, et refusez un horodatage de plus de 5 minutes.

Python

import hashlib, hmac, time

def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, v1 = parts["t"], parts["v1"]
    if abs(time.time() - int(t)) > tolerance:
        return False  # trop ancien : rejeu possible
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

# body = les octets bruts reçus, AVANT tout décodage JSON

Node.js

import crypto from "node:crypto";

export function verify(secret, header, rawBody, tolerance = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > tolerance) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.`)
    .update(rawBody) // Buffer des octets reçus, avant JSON.parse
    .digest("hex");
  return expected.length === parts.v1.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Ce qu'on attend de votre adresse