Webhooks
Abonnez un point de terminaison HTTPS de votre côté et SecureCodingHub POSTera une enveloppe JSON signée à celui-ci chaque fois que les événements qui vous intéressent se produisent. Chaque livraison est signée avec HMAC-SHA256 pour que vous puissiez vérifier qu'elle vient de nous, et non d'un usurpateur.
Quand utiliser les webhooks
- Ouvrir un ticket Jira chaque fois qu'un développeur termine (ou échoue à) une affectation obligatoire.
- Mettre à jour un enregistrement RH lorsqu'un certificat de complétion de catégorie est émis.
- Notifier Slack lorsqu'une exécution SARIF de CI crée automatiquement des affectations de formation.
- Refléter l'état SCH dans votre propre entrepôt de données sans interroger l'API REST.
Création d'un point de terminaison
Connectez-vous en tant qu'administrateur d'organisation et ouvrez Organisation → Webhooks.
Cliquez sur + Ajouter un point de terminaison. Saisissez l'URL HTTPS publique qui recevra les livraisons. Les tunnels Ngrok / cloudflared conviennent pour le développement local.
Cochez les événements auxquels vous voulez vous abonner (vous pouvez changer cela plus tard).
Cliquez sur Créer le point de terminaison. Un secret de signature whsec_… est affiché une fois. Copiez-le dans votre gestionnaire de secrets — c'est la clé que vous utiliserez pour vérifier chaque livraison.
Catalogue d'événements
v1 publie quatre types d'événements. Le catalogue est aussi disponible par programme à GET /api/public/v1/webhook-endpoints/events.
| Événement | Se déclenche quand | Forme data de la charge utile |
|---|---|---|
assignment.created | Une affectation est créée — manuellement par un administrateur, via l'API publique ou comme effet secondaire de l'ingestion SARIF. | Même forme que la réponse POST /assignments. |
assignment.completed | Un utilisateur atteint 100 % de complétion sur une affectation qui lui a été donnée. | Résumé d'affectation plus userId, userName, userEmail, completedAt. |
sarif.ingested | Une exécution SARIF a été traitée (que des affectations aient été créées à partir d'elle ou non). | Même forme que la réponse POST /sarif synchrone. |
certificate.issued | Un certificat de complétion de catégorie est émis automatiquement à un utilisateur. | certificateId, certificateNumber, détails de l'utilisateur, categoryId, categoryTitle, issuedAt. |
Forme de la requête
Chaque livraison est un POST avec ces en-têtes et un corps JSON :
POST /your-endpoint HTTP/1.1
Host: example.com
Content-Type: application/json
User-Agent: SecureCodingHub-Webhooks/1.0
X-SecureCodingHub-Event: assignment.completed
X-SecureCodingHub-Event-Id: evt_8a3d…
X-SecureCodingHub-Delivery: 9c1b…
X-SecureCodingHub-Signature: t=1717003245,v1=ad8c…
{
"id": "evt_8a3d…",
"type": "assignment.completed",
"createdAt": "2026-05-29T13:45:12.412Z",
"orgId": "e188fc87-…",
"data": { … }
}L'enveloppe est identique à travers les types d'événements. Seuls type et le data interne changent.
Vérification de la signature
L'en-tête X-SecureCodingHub-Signature a le format t=<unix_seconds>,v1=<hex>. Pour vérifier :
Analysez t et v1 depuis l'en-tête.
Calculez HMAC-SHA256("<t>.<raw_body>", secret) comme une chaîne hexadécimale en minuscules.
Comparez à v1 en temps constant.
Rejetez si l'horodatage est à plus de 5 minutes de l'horloge de votre serveur (défense contre la rediffusion).
Vérifiez toujours par rapport au corps de requête brut — re-sérialiser le JSON changera l'ordre des octets et invalidera la signature.
Exemple Node.js
import crypto from 'crypto'
import express from 'express'
const app = express()
const SECRET = process.env.SCH_WEBHOOK_SECRET
app.post('/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const header = req.header('X-SecureCodingHub-Signature') || ''
const parts = Object.fromEntries(
header.split(',').map(p => p.trim().split('='))
)
const ts = parseInt(parts.t, 10)
const sig = parts.v1
if (!ts || !sig) return res.status(400).send('bad signature header')
if (Math.abs(Date.now() / 1000 - ts) > 300) {
return res.status(400).send('stale')
}
const expected = crypto
.createHmac('sha256', SECRET)
.update(`${ts}.${req.body.toString('utf8')}`)
.digest('hex')
const ok = crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(sig, 'hex')
)
if (!ok) return res.status(401).send('bad signature')
const event = JSON.parse(req.body.toString('utf8'))
console.log(`✓ ${event.type} ${event.id}`)
res.json({ received: true })
})
app.listen(7777)Exemple Python (Flask)
import hashlib, hmac, os, time
from flask import Flask, request, abort
SECRET = os.environ['SCH_WEBHOOK_SECRET'].encode()
app = Flask(__name__)
@app.post('/webhook')
def webhook():
header = request.headers.get('X-SecureCodingHub-Signature', '')
parts = dict(p.split('=') for p in header.split(','))
try:
ts = int(parts['t']); sig = parts['v1']
except (KeyError, ValueError):
abort(400, 'bad signature header')
if abs(time.time() - ts) > 300:
abort(400, 'stale')
raw = request.get_data() # raw bytes
expected = hmac.new(SECRET, f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(401, 'bad signature')
event = request.get_json()
print('OK', event['type'], event['id'])
return {'received': True}Exemple Go
package main
import (
"crypto/hmac"; "crypto/sha256"; "encoding/hex"; "io"
"net/http"; "os"; "strconv"; "strings"; "time"
)
var secret = []byte(os.Getenv("SCH_WEBHOOK_SECRET"))
func webhook(w http.ResponseWriter, r *http.Request) {
header := r.Header.Get("X-SecureCodingHub-Signature")
var ts int64; var sig string
for _, p := range strings.Split(header, ",") {
kv := strings.SplitN(strings.TrimSpace(p), "=", 2)
if len(kv) != 2 { continue }
if kv[0] == "t" { ts, _ = strconv.ParseInt(kv[1], 10, 64) }
if kv[0] == "v1" { sig = kv[1] }
}
if ts == 0 || sig == "" { http.Error(w, "bad header", 400); return }
if abs(time.Now().Unix() - ts) > 300 { http.Error(w, "stale", 400); return }
body, _ := io.ReadAll(r.Body)
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(strconv.FormatInt(ts, 10) + "."))
mac.Write(body)
if !hmac.Equal(mac.Sum(nil), mustHex(sig)) {
http.Error(w, "bad signature", 401); return
}
w.Write([]byte(`{"received":true}`))
}
func mustHex(s string) []byte { b, _ := hex.DecodeString(s); return b }
func abs(x int64) int64 { if x < 0 { return -x }; return x }
func main() {
http.HandleFunc("/webhook", webhook)
http.ListenAndServe(":7777", nil)
}Garanties de livraison et réessais
Les livraisons webhook sont mises en file d'attente et dispatchées de manière asynchrone par un worker en arrière-plan. Elles sont au-moins-une-fois — dans des circonstances rares (timeouts, courses de réponses) le même événement peut être livré deux fois. Utilisez le champ id sur l'enveloppe (et l'en-tête X-SecureCodingHub-Event-Id) pour dédupliquer.
Votre point de terminaison devrait répondre avec un statut 2xx quelconque dans les 15 secondes. Tout autre chose est traité comme un échec et déclenche un réessai avec backoff exponentiel :
| Tentative | Temps après le précédent |
|---|---|
| 1 (initiale) | mise en file immédiatement après l'événement source |
| 2 | +1 minute |
| 3 | +5 minutes |
| 4 | +30 minutes |
| 5 (finale) | +2 heures |
Après le 5e échec, la livraison est marquée exhausted et le point de terminaison est automatiquement désactivé. Vous verrez les échecs et l'horodatage d'auto-désactivation sous Organisation → Webhooks → Voir les livraisons.
Bonnes pratiques
- Vérifiez toujours la signature. Quiconque devine votre URL pourrait autrement spammer de faux événements dans votre pipeline.
- Répondez vite. Transférez le travail vers une file d'attente et renvoyez
200en quelques millisecondes — ne traitez pas de manière synchrone à l'intérieur du gestionnaire. - Dédupliquez par
event.id. Stockez les IDs vus pendant au moins 7 jours. - Ne faites pas confiance au filtrage basé sur IP. Les IP de sortie Cloudflare peuvent changer sans préavis ; la signature est le seul point d'ancrage de confiance stable.
- Testez avant de passer en production. Utilisez cloudflared (
cloudflared tunnel --url http://localhost:7777) ou ngrok pour exposer un récepteur local, puis déclenchez un événement via l'API pour capturer une livraison de bout en bout.
Inspection des livraisons récentes
La console d'administration liste les 50 livraisons les plus récentes par point de terminaison avec statut, nombre de tentatives, code de réponse et tout message d'erreur. L'accès programmatique est disponible à :
GET /api/public/v1/webhook-endpoints/{id}/deliveriesCela nécessite la portée webhook:manage.