Aicscan Documentation

Webhooks

Un canal webhook envoie chaque notification en POST JSON sur votre serveur HTTPS. Chaque requête est signée avec le secret du canal : vérifiez toujours la signature avant de traiter le contenu.

Pour créer le canal, voir Canaux. Le secret (whsec_…) est renvoyé une seule fois, à la création ; il est stocké chiffré par Aicscan.

Requête envoyée#

HTTP
POST /aicscan HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: Aicscan-Webhook/1
X-Event-Id: 0193a1c9-41aa-7b2e-9c0d-1e2f3a4b5c6d
X-Timestamp: 1794648613
X-Signature: sha256=7c1e0f6b2a…
En-tête Contenu
X-Event-Id Identifiant de la notification. Identique à chaque relance : sert à ignorer un doublon.
X-Timestamp Heure de cet envoi, en secondes Unix (UTC). Change à chaque tentative.
X-Signature sha256= suivi du HMAC-SHA256, en hexadécimal minuscule, de la chaîne "{X-Timestamp}.{corps brut}", calculé avec le secret du canal.

Corps#

JSON
{
  "id": "0193a1c9-41aa-7b2e-9c0d-1e2f3a4b5c6d",
  "type": "alert",
  "stage": "confirmed",
  "severity": "critical",
  "priority": 30,
  "attempt": 1,
  "timestamp": 1794648613,
  "title": "🔴 CRITICAL · Treasury: large USDT outflows · outgoing",
  "text": "🔴 CRITICAL · Treasury: large USDT outflows · outgoing\n✅ Confirmed · 15 confirmations\nChain: BNB Chain\nAddress: Treasury (0x8894…d4e3)\nAmount: 250,000 USDT (≈ $250,000)\nTo: 0x77cd…01e2 (unknown)\nBlock 45,123,456\nDetails: https://aicscan.io/e/0193a1c9-3e02-7f3a-9b4c-5d6e7f8091a2"
}
Champ Sens
id Identifiant de la notification (égal à X-Event-Id). Détail complet : GET /v1/notifications/{id}.
type alert, digest, test, security, summary ou service (voir Format des alertes).
stage Pour une alerte : unconfirmed, confirmed, cancelled ou escalation. Pour les autres types, reprend le type.
severity critical, warning ou info.
priority Priorité numérique (30 et plus : critique).
attempt Numéro de la tentative (1 au premier envoi).
timestamp Égal à X-Timestamp.
title Première ligne du message.
text Message complet, en texte brut, dans la langue du canal.

Ignorez les champs et les valeurs que vous ne connaissez pas : de nouveaux peuvent apparaître (voir Versionnement).

Vérifier la signature#

  1. Lisez le corps brut, octet pour octet, avant toute analyse JSON : une analyse puis une nouvelle sérialisation change les octets et invalide la signature.
  2. Calculez HMAC-SHA256(secret, X-Timestamp + "." + corps brut) et comparez "sha256=" + hex à X-Signature en temps constant.
  3. Rejetez la requête si X-Timestamp s’écarte de plus de 300 secondes de votre horloge.
  4. Ignorez un X-Event-Id déjà traité.
  5. Répondez 2xx en moins de 10 secondes.

Le secret s’utilise tel quel, préfixe whsec_ compris, comme clé HMAC (ses octets UTF-8).

Récepteurs complets#

Chaque exemple démarre un serveur HTTP sur le port 3000, lit le secret dans AICSCAN_WEBHOOK_SECRET et applique les cinq règles ci-dessus.

Node.js
// Récepteur de webhooks Aicscan (Node.js 18+, sans dépendance).
import crypto from 'node:crypto';
import http from 'node:http';

const SECRET = process.env.AICSCAN_WEBHOOK_SECRET; // whsec_…, affiché à la création du canal
const TOLERANCE_SECONDS = 300;
const seen = new Set(); // en production : table ou cache partagé, avec expiration

export function verifySignature(secret, rawBody, timestamp, signature, now = Date.now() / 1000) {
  if (typeof timestamp !== 'string' || !/^\d+$/.test(timestamp)) return false;
  if (Math.abs(now - Number(timestamp)) > TOLERANCE_SECONDS) return false;
  const expected =
    'sha256=' +
    crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(typeof signature === 'string' ? signature : '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const server = http.createServer((req, res) => {
  if (req.method !== 'POST') return res.writeHead(405).end();
  const chunks = [];
  req.on('data', (c) => chunks.push(c));
  req.on('end', () => {
    const raw = Buffer.concat(chunks); // corps brut, avant JSON.parse
    const ok = verifySignature(SECRET, raw, req.headers['x-timestamp'], req.headers['x-signature']);
    if (!ok) return res.writeHead(401).end();

    const eventId = req.headers['x-event-id'];
    if (seen.has(eventId)) return res.writeHead(200).end(); // relance déjà traitée
    seen.add(eventId);

    const event = JSON.parse(raw);
    console.log(event.type, event.stage, event.severity, event.title);
    res.writeHead(204).end(); // répondre en moins de 10 s ; traiter ensuite si besoin
  });
});
server.listen(process.env.PORT ?? 3000);

Les quatre exemples ont été exécutés (Node.js 20, Python 3, PHP 8.3, Go 1.23) contre des requêtes signées avec openssl : requête valide acceptée ; corps modifié, horodatage modifié, horodatage de plus de 5 minutes et signature absente refusés (401). Les fonctions de vérification Node.js et Python reconnaissent aussi le vecteur de référence du code d’Aicscan ci-dessous. L’exemple PHP, sans état entre deux requêtes, laisse la détection des doublons à votre base de données.

Vecteur de test#

Pour tester votre implémentation sans attendre une alerte :

Entrée Valeur
Secret whsec_test
X-Timestamp 1700000000
Corps brut {"a":1}
X-Signature attendue sha256=38877139021993b830af32feea6e18a8da83eb2f6e49ee50bd9e4cf4ca4d3789
Shell
printf '1700000000.{"a":1}' | openssl dgst -sha256 -hmac whsec_test
# SHA2-256(stdin)= 38877139021993b830af32feea6e18a8da83eb2f6e49ee50bd9e4cf4ca4d3789

Pour ce vecteur, passez « maintenant » = 1700000000 à votre fonction de vérification, sinon la fenêtre de 5 minutes le rejette.

Protection contre le rejeu#

L’horodatage fait partie de la chaîne signée : un attaquant qui a intercepté une requête ne peut pas la rejouer avec un horodatage récent sans connaître le secret. En refusant tout X-Timestamp à plus de 300 secondes de votre horloge, vous bornez la fenêtre de rejeu à 5 minutes ; en mémorisant les X-Event-Id traités (au moins 24 heures, la durée des relances), vous la fermez complètement.

Gardez l’horloge de votre serveur synchronisée (NTP).

Réponse attendue et relances#

Votre réponse Effet
2xx en moins de 10 s Succès.
404 ou 410 Échec définitif : plus de relance pour cette notification.
429 avec Retry-After Le canal attend le délai demandé avant la tentative suivante.
3xx Échec : les redirections ne sont pas suivies. Donnez l’URL finale.
Autre statut, délai dépassé, erreur réseau Relance.

Les relances suivent ce calendrier : 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, puis toutes les 6 h, sans dépasser 24 heures. La réponse de votre serveur est lue sur 64 Kio au plus ; ses 200 premiers caractères sont conservés dans le journal des tentatives (GET /v1/notifications/{id}).

Répondez vite, puis traitez : placez l’alerte dans une file (base de données, file de messages) et répondez 204 ; un traitement long dépasserait les 10 secondes et provoquerait une relance.

Désactivation automatique#

Après 24 heures d’échecs continus, le canal est désactivé (state: "disabled", motif dans disabled_reason) et le propriétaire de l’espace est prévenu par email. Une fois la cause corrigée, réactivez-le :

Shell
curl -X PATCH https://aicscan.io/v1/channels/$CHANNEL_ID \
  -H "Authorization: Bearer $AICSCAN_API_KEY" -H "Content-Type: application/json" \
  -d '{"state":"active"}'

Puis renvoyez les notifications manquées avec POST /v1/notifications/{id}/resend (liste avec GET /v1/notifications?channel_id=…&status=failed).

Sécurité de l’URL#

À la création et à chaque envoi, l’URL est contrôlée :

  • https uniquement, sans identifiants dans l’URL ;
  • noms refusés : localhost, *.localhost, *.local, *.internal, *.lan, *.home.arpa et noms sans point ;
  • le nom est résolu au moment de l’envoi ; si une seule des adresses obtenues est privée, locale, de lien local (dont 169.254.169.254), CGNAT, réservée, multicast ou de documentation, l’envoi est refusé ;
  • la connexion est faite vers l’adresse vérifiée, sans seconde résolution DNS ni proxy.

Si votre récepteur filtre par adresse IP source, prévoyez que ces adresses puissent changer : fiez-vous à la signature, pas à l’adresse.

Aicscan ne vous demandera jamais de vous connecter à un wallet, de signer une transaction ni d’envoyer des fonds.

aicscan.io · Statut · openapi.yaml