Skip to content

Webhooks

Instead of polling, you can have Accessful POST a signed event to your server the moment a job changes state.

There is no dashboard and no registration endpoint. You attach a callback per upload, and you choose the signing secret — Accessful does not issue one.

  • Multipart upload — send form fields webhookUrl and secret.
  • Upload by URL — send JSON fields callbackUrl and hmacSignature.
Terminal window
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://your-app.example.com/hooks/accessful" \
-F "secret=$YOUR_WEBHOOK_SECRET"
Event type Fires when
case.queued The PDF was accepted and entered the analysis queue. Fires once, carrying the starting queue position.
case.running The job started processing. May fire more than once (one per iteration).
case.completed The PDF/UA result is ready to download.
case.failed Processing failed.
case.canceled The case was canceled.
case.quota_exceeded Rejected because the contract quota is exhausted.

The quota_pending state is not delivered as a webhook.

The request body is 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"
}
}
Field Type Notes
id UUID Event ID. Use it as the idempotency key (see below).
type string One of the event types above.
apiVersion string Contract version (2026-06-26). Pin it to detect changes.
occurredAt ISO-8601 When the event was created; stable across retries.
data.caseId UUID The case this event is about.
data.fileName string The file name.
data.jobStatus string Raw job status, e.g. queued, completed, failed, canceled, quota_exceeded.
data.queuePosition integer case.queued only — 1-based starting rank in the queue.
data.queueTotal integer case.queued only — total cases waiting at that moment.
data.estimatedWaitSeconds integer case.queued only — best-effort wait estimate in seconds.

Null fields are omitted from the JSON.

The case.queued event is the push counterpart to the job-status queue position: it fires once, the moment a PDF is accepted, and carries the starting position so you can show “you’re 7th in line, ~5 min” without polling.

{
"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
}
}

This is a discrete lifecycle event, not a live stream — the position is a snapshot at acceptance time and is not re-pushed as the queue advances. For a continuously updating position, poll GET /job-status/{caseId}, whose response carries the same queue* fields while the case is waiting. The queue* fields appear only on case.queued; every other event omits them.

Header Example Purpose
X-Accessful-Signature t=1749126896,v1=9f86d0… HMAC signature — verify this
X-Accessful-Webhook-Timestamp 1749126896 Unix seconds; same as t= above
X-Accessful-Event-Id f1d2c3b4-… Equals id; idempotency key
X-Accessful-Event-Type case.completed Routing without parsing the body
X-Accessful-Case-Id 7c2f1e4a-… The case ID
X-Accessful-Delivery-Attempt 1 Attempt counter, 1-based
X-Signature n4bQgY… legacy base64 HMAC over body only

The X-Accessful-Signature header has the form t=<unix>,v1=<hex>. Recompute it and compare in constant time:

  1. Read t and v1 from the header.
  2. Compute HMAC-SHA256(secret, "<t>.<raw body>") and hex-encode it. The signed string is the timestamp, a literal dot, then the exact raw request body — verify before JSON parsing.
  3. Constant-time compare against v1.
  4. Optionally reject if t is older than a few minutes (replay protection).
import crypto from 'node:crypto';
// `rawBody` must be the exact bytes received (e.g. 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;
}
  • Success = any 2xx response within the timeout (5s to connect, 10s to respond). Answer fast and do your work asynchronously.
  • Failure (non-2xx, timeout, or connection error) is retried up to 10 attempts with exponential backoff and jitter — roughly 10s → 30s → 1.5m → 4.5m → 13.5m → 40m → 2h, then capped at 6h, minus up to 20% jitter. The whole window spans several hours.
  • After 10 failed attempts the event is given up. Contact support to redeliver.
  • Idempotency: the same event may arrive more than once (e.g. a retry after your endpoint already succeeded). De-duplicate on X-Accessful-Event-Id — it is stable across every retry.