Webhooks
Au lieu d’interroger, vous pouvez demander à Accessful d’envoyer par POST un événement signé à votre serveur dès qu’une tâche change d’état.
Comment fonctionne l’enregistrement
Section intitulée « Comment fonctionne l’enregistrement »Il n’y a ni tableau de bord ni endpoint d’enregistrement. Vous attachez un callback à chaque téléversement, et vous choisissez vous-même le secret de signature — Accessful n’en émet aucun.
- Téléversement multipart — envoyez les champs de formulaire
webhookUrletsecret. - Téléversement par URL — envoyez les champs JSON
callbackUrlethmacSignature.
curl -X POST "https://api.accessful.de/api/v1/upload-service/pdf/upload" \ -H "X-API-Key: $ACCESSFUL_API_KEY" \ -F "files=@document.pdf" \ -F "webhookUrl=https://votre-app.example.com/hooks/accessful" \ -F "secret=$VOTRE_WEBHOOK_SECRET"Événements
Section intitulée « Événements »type d’événement |
Se déclenche quand |
|---|---|
case.queued |
Le PDF a été accepté et est entré dans la file d’analyse. Se déclenche une seule fois et contient la position dans la file de départ. |
case.running |
La tâche a commencé son traitement. Peut se déclencher plusieurs fois (une par itération). |
case.completed |
Le résultat PDF/UA est prêt à être téléchargé. |
case.failed |
Le traitement a échoué. |
case.canceled |
Le cas a été annulé. |
case.quota_exceeded |
Rejeté car le quota contractuel est épuisé. |
L’état quota_pending n’est pas livré en tant que webhook.
Le corps de la requête est au format JSON. Content-Type: application/json.
{ "id": "f1d2c3b4-0000-4a1e-8f3c-2d6b5a9e1c40", "type": "case.completed", "apiVersion": "2026-06-26", "occurredAt": "2026-06-26T12:34:56Z", "data": { "caseId": "7c2f1e4a-9b0d-4a1e-8f3c-2d6b5a9e1c40", "fileName": "document.pdf", "jobStatus": "completed" }}| Champ | Type | Notes |
|---|---|---|
id |
UUID | ID de l’événement. Utilisez-le comme clé d’idempotence (voir ci-dessous). |
type |
string | L’un des types d’événement ci-dessus. |
apiVersion |
string | Version du contrat (2026-06-26). Épinglez-la pour détecter les changements. |
occurredAt |
ISO-8601 | Date de création de l’événement ; stable d’une tentative à l’autre. |
data.caseId |
UUID | Le cas concerné par cet événement. |
data.fileName |
string | Le nom du fichier. |
data.jobStatus |
string | Statut brut de la tâche, p. ex. queued, completed, failed, canceled, quota_exceeded. |
data.queuePosition |
integer | case.queued uniquement — rang de départ dans la file, à partir de 1. |
data.queueTotal |
integer | case.queued uniquement — nombre total de cas en attente à ce moment-là. |
data.estimatedWaitSeconds |
integer | case.queued uniquement — estimation indicative du temps d’attente en secondes. |
Les champs nuls sont omis du JSON.
Position dans la file sur case.queued
Section intitulée « Position dans la file sur case.queued »L’événement case.queued est le pendant push de la position dans la file du statut de
tâche : il se déclenche une seule fois,
au moment où un PDF est accepté, et contient la position de départ afin que vous puissiez
afficher « vous êtes 7e dans la file, ~5 min » sans interrogation.
{ "id": "a3b1c2d4-0000-4a1e-8f3c-2d6b5a9e1c40", "type": "case.queued", "apiVersion": "2026-06-26", "occurredAt": "2026-06-26T12:30:00Z", "data": { "caseId": "7c2f1e4a-9b0d-4a1e-8f3c-2d6b5a9e1c40", "fileName": "document.pdf", "jobStatus": "queued", "queuePosition": 7, "queueTotal": 23, "estimatedWaitSeconds": 300 }}Il s’agit d’un événement de cycle de vie discret, pas d’un flux en temps réel — la
position est un instantané au moment de l’acceptation et n’est pas renvoyée à mesure que la
file avance. Pour une position continuellement mise à jour, interrogez GET /job-status/{caseId},
dont la réponse porte les mêmes champs queue* tant que le cas est en attente. Les champs
queue* n’apparaissent que sur case.queued ; tout autre événement les omet.
En-têtes de livraison
Section intitulée « En-têtes de livraison »| En-tête | Exemple | Objet |
|---|---|---|
X-Accessful-Signature |
t=1749126896,v1=9f86d0… |
Signature HMAC — à vérifier |
X-Accessful-Webhook-Timestamp |
1749126896 |
Secondes Unix ; identique au t= ci-dessus |
X-Accessful-Event-Id |
f1d2c3b4-… |
Égale id ; clé d’idempotence |
X-Accessful-Event-Type |
case.completed |
Routage sans analyser le corps |
X-Accessful-Case-Id |
7c2f1e4a-… |
L’ID du cas |
X-Accessful-Delivery-Attempt |
1 |
Compteur de tentatives, à partir de 1 |
X-Signature |
n4bQgY… |
legacy HMAC base64 sur le corps uniquement |
Vérifier la signature
Section intitulée « Vérifier la signature »L’en-tête X-Accessful-Signature a la forme t=<unix>,v1=<hex>. Recalculez-la et comparez en
temps constant :
- Lisez
tetv1dans l’en-tête. - Calculez
HMAC-SHA256(secret, "<t>.<corps brut>")et encodez-le en hexadécimal. La chaîne signée est l’horodatage, un point littéral, puis le corps brut exact de la requête — vérifiez avant l’analyse JSON. - Comparez en temps constant avec
v1. - Éventuellement, rejetez si
tdate de plus de quelques minutes (protection contre le rejeu).
import crypto from 'node:crypto';
// `rawBody` doit correspondre aux octets exacts reçus (p. ex. express.raw()).function verify(rawBody, signatureHeader, secret) { const parts = Object.fromEntries(signatureHeader.split(',').map((p) => p.split('='))); const expected = crypto .createHmac('sha256', secret) .update(`${parts.t}.${rawBody}`) .digest('hex');
const valid = crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)); const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; // 5 min return valid && fresh;}String[] kv = signatureHeader.split(","); // ["t=...", "v1=..."]long t = Long.parseLong(kv[0].substring(2));String v1 = kv[1].substring(3);
Mac mac = Mac.getInstance("HmacSHA256");mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));byte[] sig = mac.doFinal((t + "." + rawBody).getBytes(StandardCharsets.UTF_8));String expected = HexFormat.of().formatHex(sig);
boolean valid = MessageDigest.isEqual( expected.getBytes(StandardCharsets.UTF_8), v1.getBytes(StandardCharsets.UTF_8));Nouvelles tentatives et idempotence
Section intitulée « Nouvelles tentatives et idempotence »- Succès = toute réponse 2xx dans le délai imparti (5 s pour se connecter, 10 s pour répondre). Répondez vite et effectuez votre travail de manière asynchrone.
- Échec (non-2xx, délai dépassé ou erreur de connexion) est réessayé jusqu’à 10
tentatives avec un backoff exponentiel et du jitter — environ
10s → 30s → 1,5m → 4,5m → 13,5m → 40m → 2h, puis plafonné à 6h, moins jusqu’à 20 % de jitter. La fenêtre complète s’étend sur plusieurs heures. - Après 10 tentatives échouées, l’événement est abandonné. Contactez le support pour une nouvelle livraison.
- Idempotence : le même événement peut arriver plus d’une fois (p. ex. une nouvelle
tentative après que votre endpoint a déjà réussi). Dédupliquez sur
X-Accessful-Event-Id— il est stable à chaque tentative.