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 typeFires when
case.runningThe job started processing. May fire more than once (one per iteration).
case.completedThe PDF/UA result is ready to download.
case.failedProcessing failed.
case.canceledThe case was canceled.
case.quota_exceededRejected because the contract quota is exhausted.

The queued and quota_pending states are not delivered as webhooks.

The request body is JSON. Content-Type: application/json.

{
"id": "f1d2c3b4-0000-4a1e-8f3c-2d6b5a9e1c40",
"type": "case.completed",
"apiVersion": "2026-06-05",
"occurredAt": "2026-06-05T12:34:56Z",
"data": {
"caseId": "7c2f1e4a-9b0d-4a1e-8f3c-2d6b5a9e1c40",
"fileName": "document.pdf",
"jobStatus": "completed"
}
}
FieldTypeNotes
idUUIDEvent ID. Use it as the idempotency key (see below).
typestringOne of the event types above.
apiVersionstringContract version (2026-06-05). Pin it to detect changes.
occurredAtISO-8601When the event was created; stable across retries.
data.caseIdUUIDThe case this event is about.
data.fileNamestringThe file name.
data.jobStatusstringRaw job status, e.g. completed, failed, canceled, quota_exceeded.

Null fields are omitted from the JSON.

HeaderExamplePurpose
X-Accessful-Signaturet=1749126896,v1=9f86d0…HMAC signature — verify this
X-Accessful-Webhook-Timestamp1749126896Unix seconds; same as t= above
X-Accessful-Event-Idf1d2c3b4-…Equals id; idempotency key
X-Accessful-Event-Typecase.completedRouting without parsing the body
X-Accessful-Case-Id7c2f1e4a-…The case ID
X-Accessful-Delivery-Attempt1Attempt counter, 1-based
X-Signaturen4bQgY…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.