Webhooks
Instead of polling, you can have Accessful POST a signed event to your server the moment a job changes state.
How registration works
Section titled “How registration works”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
webhookUrlandsecret. - Upload by URL — send JSON fields
callbackUrlandhmacSignature.
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"Events
Section titled “Events”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.
Payload
Section titled “Payload”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.
Queue position on case.queued
Section titled “Queue position on case.queued”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.
Delivery headers
Section titled “Delivery headers”| 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 |
Verify the signature
Section titled “Verify the signature”The X-Accessful-Signature header has the form t=<unix>,v1=<hex>. Recompute it and
compare in constant time:
- Read
tandv1from the header. - 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. - Constant-time compare against
v1. - Optionally reject if
tis 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;}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));Retries & idempotency
Section titled “Retries & idempotency”- 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.