Webhooks
ExtentAPI peut vous notifier en push à la complétion d’un job ou d’une extraction. Plus rapide que le polling, plus économe en crédits, et signé HMAC SHA-256 pour vérifier l’authenticité.
Modèle : webhook par-requête (pas d’abonnement)
Section intitulée « Modèle : webhook par-requête (pas d’abonnement) »Il n’y a pas d’endpoint d’abonnement : vous attachez un webhook
directement à la requête via le champ webhookUrl. Chaque job / extraction
livre une seule fois, sur son état terminal (done ou failed).
curl -X POST https://api.extentapi.example/v1/scraper/jobs \ -H "x-api-key: apk_live_..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://exemple.fr/article", "webhookUrl": "https://api.exemple.fr/hooks/extentapi", "webhookSecret": "whsec_un_secret_d_au_moins_16_caracteres" }'Pour suivre le cycle de vie complet d’un job (created / claimed / …) plutôt
que le seul état terminal, utilisez le flux SSE GET /events.
Le secret de signature
Section intitulée « Le secret de signature »- Vous fournissez
webhookSecret(≥ 16 caractères) à la création : il sert tel quel à signer les deliveries. Vous le connaissez déjà → vous pouvez vérifier la signature immédiatement. Recommandé. - Sinon, ExtentAPI en génère un et le renvoie une seule fois dans la
réponse de création, champ
data.webhookSecret. Stockez-le : il n’est plus jamais affiché (ni viaGET …/webhooks/:id).
{ "status": "success", "code": "201-created", "data": { "id": "job_01H...", "status": "queued", "webhookSecret": "8f3c...generated...once" }}Le secret est par-requête et reproductible : un redeliver re-signe le même
payload avec le même secret.
Extractions
Section intitulée « Extractions »Pour POST /v1/extractor/extractions, mêmes champs webhookUrl /
webhookSecret. Par défaut vous recevez les états done et dead ; ajoutez
webhookEventTypes: ["done", "failed", "dead"] pour aussi recevoir les échecs
transient (failed, avant retry).
Format des deliveries
Section intitulée « Format des deliveries »Chaque event est posté en POST sur votre webhookUrl avec :
POST /hooks/extentapi HTTP/1.1Content-Type: application/jsonUser-Agent: extentapi-webhook/1.0X-ExtentAPI-Webhook-Version: 1X-ExtentAPI-Jobid: job_01H...X-ExtentAPI-Timestamp: 1716288000X-ExtentAPI-Signature: sha256=4f2a8b...Corps (scraper — done ou failed) :
{ "jobId": "job_01H...", "status": "done", "httpStatus": 200, "finalUrl": "https://exemple.fr/article", "classification": null, "durationMs": 1843, "requestId": "req_01H...", "deliveredAt": "2026-05-21T10:35:12.000Z"}Pour une extraction réussie, le corps porte directement data (typé
article / product) et metadata — pas besoin d’un GET de suivi :
{ "extractionId": "ext_01H...", "type": "article", "status": "done", "url": "https://exemple.fr/article", "data": { "title": "...", "content": "...", "author": "..." }, "metadata": { "inputFormat": "html", "strategiesUsed": ["..."] }, "requestId": "req_01H...", "deliveredAt": "2026-05-21T10:35:12.000Z"}Headers extraction :
X-ExtentAPI-ExtractionidetX-ExtentAPI-Extraction-Webhook-Version: 1(au lieu de-Jobid/-Webhook-Version).
Vérifier la signature
Section intitulée « Vérifier la signature »La signature est un HMAC SHA-256 sur "<timestamp>.<corps brut>" (le
timestamp signé permet de rejeter les rejeux). Vérifiez toujours sur les
octets bruts reçus — ne re-sérialisez pas le JSON (la re-sérialisation
n’est pas garantie identique octet pour octet).
import crypto from "node:crypto";
// `rawBody` = Buffer des octets bruts du POST, LU AVANT tout JSON.parse.function verify(rawBody, headers, secret) { // 1. Anti-rejeu : timestamp à ±5 min. const ts = Number(headers["x-extentapi-timestamp"]); if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) { return false; } // 2. Signature sur `${ts}.${rawBody}` (retirer le préfixe `sha256=`). const provided = (headers["x-extentapi-signature"] ?? "").replace(/^sha256=/, ""); const expected = crypto .createHmac("sha256", secret) .update(`${ts}.${rawBody.toString("utf8")}`) .digest("hex"); return ( provided.length === expected.length && crypto.timingSafeEqual(Buffer.from(provided, "hex"), Buffer.from(expected, "hex")) );}Retry et delivery
Section intitulée « Retry et delivery »- À chaque échec (
!= 2xx), retry avec backoff exponentiel (jusqu’à ~7 h, puis marquéfailedet conservé pour audit). - Un même
(jobId, status)peut arriver plusieurs fois (retries internes, redelivers) : dédupliquez côté consumer surX-ExtentAPI-Jobid+ le statut final, et conservez l’historique des deliveries reçues ≥ 24 h. - Replay manuel possible via le control-plane (
POST /v1/scraper/webhooks/:id/redeliver, super-admin) — re-signe le même payload,X-ExtentAPI-Timestamprafraîchi.
Événements
Section intitulée « Événements »Les webhooks ne se déclenchent que sur états terminaux :
- Scraper :
done,failed(champstatusdu payload). - Extractor :
done,dead, etfailed(opt-in viawebhookEventTypes).
Le type d’événement se lit dans le champ status du corps — il n’y a
pas de header dédié (x-extentapi-event). Branchez votre routage sur
status (done/failed/dead), pas sur un nom d’event.
Le cycle de vie complet (created, claimed, …) n’est pas poussé en
webhook — il est disponible sur le flux SSE.
Alternative SSE
Section intitulée « Alternative SSE »Pour recevoir les événements sans exposer d’endpoint public (utile en
développement local, sans contrainte anti-SSRF), ouvrez un flux sortant
GET /v1/… /events (Server-Sent Events). Le flux est filtré server-side
par votre clé API et porte le cycle de vie complet (job.created, job.done,
job.failed, …).
En développement local, la validation anti-SSRF refuse par défaut les
webhookUrlprivées/loopback. Préférez SSE, ou demandez à l’opérateur d’activer l’escape-hatch devAPOPHIS_ALLOW_PRIVATE_WEBHOOK_TARGETS(interdit en production).
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Idempotency — dédupliquez les deliveries répétées.
- Référence complète :
api/webhooks.md(payload, canonical JSON, backoff).