Aller au contenu

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).

Fenêtre de terminal
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.

  • 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 via GET …/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.

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).

Chaque event est posté en POST sur votre webhookUrl avec :

POST /hooks/extentapi HTTP/1.1
Content-Type: application/json
User-Agent: extentapi-webhook/1.0
X-ExtentAPI-Webhook-Version: 1
X-ExtentAPI-Jobid: job_01H...
X-ExtentAPI-Timestamp: 1716288000
X-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-Extractionid et X-ExtentAPI-Extraction-Webhook-Version: 1 (au lieu de -Jobid / -Webhook-Version).

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"))
);
}
  • À chaque échec (!= 2xx), retry avec backoff exponentiel (jusqu’à ~7 h, puis marqué failed et conservé pour audit).
  • Un même (jobId, status) peut arriver plusieurs fois (retries internes, redelivers) : dédupliquez côté consumer sur X-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-Timestamp rafraîchi.

Les webhooks ne se déclenchent que sur états terminaux :

  • Scraper : done, failed (champ status du payload).
  • Extractor : done, dead, et failed (opt-in via webhookEventTypes).

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.

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 webhookUrl privées/loopback. Préférez SSE, ou demandez à l’opérateur d’activer l’escape-hatch dev APOPHIS_ALLOW_PRIVATE_WEBHOOK_TARGETS (interdit en production).

  • Idempotency — dédupliquez les deliveries répétées.
  • Référence complète : api/webhooks.md (payload, canonical JSON, backoff).