Skip to content

Webhooks

Plutôt que d'interroger l'API en boucle, faites-vous prévenir.

Les quatre événements

ÉvénementQuand il part
model.trainedune version vient d'être entraînée et sert les prédictions
model.staleassez de vérités se sont accumulées, une intégration serait utile
model.refreshedune intégration a produit une nouvelle version
credits.lowvotre solde de crédits approche de zéro

Déclarer une adresse

sh
curl -X POST "$SPAVIK_BASE_URL/v1/webhooks" \
  -H "X-API-Key: $SPAVIK_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://exemple.fr/webhooks/spavik","events":["model.stale","credits.low"]}'

Le secret n'apparaît qu'une fois

La réponse à cette création contient un champ secret. C'est la seule fois où il est affiché. Rangez-le immédiatement dans votre gestionnaire de secrets : il n'existe aucun moyen de le relire ensuite.

Une même adresse ne peut être déclarée qu'une fois. Une seconde tentative reçoit 409 WEBHOOK_URL_EXISTS, avec le nom du webhook qui occupe déjà la place.

Vérifier la signature

Chaque appel est signé. Vérifiez-le avant de faire quoi que ce soit de son contenu : sans cette vérification, n'importe qui connaissant votre URL peut vous faire croire n'importe quoi.

python
from spavik.webhooks import verifier, SpavikSignatureError

@app.post("/webhooks/spavik")
async def recevoir(request):
    try:
        evenement = verifier(await request.body(), request.headers, SECRET)
    except SpavikSignatureError:
        return Response(status_code=400)
    traiter(evenement)
    return Response(status_code=200)
js
app.post('/webhooks/spavik', express.raw({ type: 'application/json' }), async (req, res) => {
  try {
    const evenement = await verifier(req.body, req.headers, process.env.SPAVIK_WEBHOOK_SECRET);
    traiter(evenement);
    res.sendStatus(200);
  } catch {
    res.sendStatus(400);
  }
});
js
app.post('/webhooks/spavik', async (c) => {
  const corps = await c.req.arrayBuffer();
  const evenement = await verifier(corps, c.req.raw.headers, c.env.SPAVIK_WEBHOOK_SECRET);
  traiter(evenement);
  return c.body(null, 200);
});

Les octets, pas l'objet

La signature porte sur les octets reçus. Un corps analysé en JSON puis réécrit ne redonne pas la même signature, même s'il contient exactement les mêmes données. D'où le express.raw et le arrayBuffer ci-dessus.

La vérification refuse aussi un horodatage trop ancien, ce qui bloque le rejeu d'un appel intercepté. La tolérance est de cinq minutes par défaut.

Suspendre plutôt que supprimer

sh
curl -X PATCH "$SPAVIK_BASE_URL/v1/webhooks/{id}" \
  -H "X-API-Key: $SPAVIK_API_KEY" \
  -H 'Content-Type: application/json' -d '{"active": false}'

Un webhook suspendu garde son secret et sa configuration. Le supprimer vous obligerait à en recréer un, donc à changer de secret.

Le champ last_status de chaque webhook vous dit ce que votre serveur a répondu au dernier appel : de quoi repérer une intégration qui casse sans avoir à fouiller vos journaux.

Un point de cloisonnement

Les webhooks se gèrent depuis une session web uniquement. Une clé d'usage qui tente d'y accéder reçoit 403. C'est délibéré : une clé déployée dans une application ne doit pas pouvoir rediriger vos notifications.

Ne confondez pas les deux sens

Ce que décrit cette page, ce sont les webhooks sortants : Spavik appelle votre serveur. La route POST /v1/integrations/stripe/webhook est un webhook entrant, appelé par Stripe, et qui ne vous concerne pas.

Documentation générée pour partie depuis le contrat OpenAPI.