Overview
The Accessful API converts PDF documents into PDF/UA (Universal Accessibility) format. PDF/UA is the ISO 14289 standard that makes PDFs usable by assistive technology.
https://api.accessful.deAPI Version: 2.1.0
Authentication:
X-API-Key headerContent Types:
application/json, multipart/form-data
Key Features
- Batch Upload: Upload multiple PDF files in one request
- URL Upload: Submit PDFs by URL — Accessful downloads and processes them
- Asynchronous Processing: Webhook notifications when jobs finish
- Single Credential: One static API key authenticates every request
- HMAC-signed Callbacks: Webhook payloads are signed for tamper-protection
- Case Management: Every uploaded PDF gets a UUID-based case ID for status & download
API Workflow
Try it out
Convert a PDF to PDF/UA right here in the browser — paste your API key, pick a
PDF, and watch the full upload → process → download roundtrip happen against
api.accessful.de.
https://api.accessful.de as the X-API-Key header,
never to a third party and never to docs.accessful.de. The PDF is
processed by Accessful exactly as it would be from your production integration.
PDF/UA Tester
- Upload
- Process
- Download
Migration v1 → v2
If you integrated against the v1 API (gateway / Keycloak OAuth2), three things change:
| What changed | v1 (before May 2026) | v2 (current) |
|---|---|---|
| Base URL | https://gateway.accessful.de |
https://api.accessful.de |
| Authentication | OAuth2 password grant: token from iam.accessful.de/realms/accessful-realm/…
using client_id=accessful-api + username + password, then
Authorization: Bearer <jwt> |
Static API key sent as the X-API-Key header. No token round-trip,
no realm, no client ID, no username, no password. |
| Path prefix | Same: /api/v1/upload-service/… |
Unchanged: /api/v1/upload-service/… |
accessful-realm and the OAuth2 client
accessful-api have been retired. Existing integrations that still hit
gateway.accessful.de or request tokens from the old realm will fail with
401 / 404.Already-running jobs are not affected — they finish under the old credentials they were started with. Only new requests need the v2 credential.
Authentication
Every request to the Accessful API must include your API key in the X-API-Key header.
An API key is a single, long-lived secret issued by your Accessful organisation admin.
X-API-Key: ak_<your_api_key>Applies to: every endpoint documented below
Rotation: Generate a new key in the Hub portal, switch over, then revoke the old one. Old and new keys can coexist during a rotation window.
Example: smoke-test your key
Use the cheapest authenticated endpoint to verify the key is alive:
You should get 404 Not Found for the made-up case ID. If you get
401 Unauthorized, the key is wrong, revoked, or being sent in the wrong header.
What if my key doesn't work?
- 401 Unauthorized: key is missing, malformed, revoked, or sent under the wrong
header name. Header must be exactly
X-API-Key, value must be the fullak_…string with no leading/trailing whitespace. - 403 Forbidden: key is valid but the user it belongs to lacks the role for the requested action. Ask your org admin to check the role assignment in the Hub portal.
- 429 Too Many Requests: rate limit hit. Back off exponentially and retry.
API Endpoints
All endpoints expect the X-API-Key header. The Authorization header
is no longer required for API-key clients.
Upload PDF Files (multipart)
POSTUpload one or more PDF files for conversion to PDF/UA. Optionally configure a webhook to be notified when each file finishes processing.
Headers
| Header | Value |
|---|---|
X-API-Key |
Your API key (ak_…) |
Content-Type |
multipart/form-data |
Form Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
files |
File[] | Yes | Array of PDF files to upload (max 100 MB per file) |
webhookUrl |
string | No | Callback URL invoked when each file finishes processing |
secret |
string | No* | HMAC-SHA256 secret used to sign webhook payloads (*required if
webhookUrl is set) |
folder-name |
string | No | Optional folder name that groups the uploaded PDFs in the Hub portal |
Response Codes
Success Response (200)
{
"successfulUploads": [
"c8c956bb-19c3-451b-826f-5b3129eee4c1",
"6848df1b-c6cf-4ae4-8ddd-c474cc103d4e"
],
"duplicateFiles": [],
"message": "Upload completed successfully. Uploaded 2 files. 0 duplicates found.",
"callbackUrl": "https://your-server.com/callback"
}Example Request
Upload PDF Files by URL
POSTUpload PDFs by URL instead of uploading bytes. Accessful downloads each URL and queues it for conversion. The request is accepted asynchronously.
Headers
| Header | Value |
|---|---|
X-API-Key |
Your API key (ak_…) |
Content-Type |
application/json |
JSON Body
| Field | Type | Required | Description |
|---|---|---|---|
files |
FileEntry[] | Yes | Array of { url, filename } entries |
callbackUrl |
string | No | Callback URL for processing notifications |
hmacSignature |
string | No* | HMAC-SHA256 secret used to sign webhook payloads
(*required if callbackUrl is set) |
FileEntry Object
| Property | Type | Required | Description |
|---|---|---|---|
url |
string | Yes | Publicly reachable URL of the PDF |
filename |
string | Yes | Filename ending with .pdf (alphanumeric, underscore, hyphen) |
Response Codes
Success Response (202)
{
"accepted": [
{
"uri": "https://example.com/document1.pdf",
"jobId": "c8c956bb-19c3-451b-826f-5b3129eee4c1"
},
{
"uri": "https://example.com/document2.pdf",
"jobId": "6848df1b-c6cf-4ae4-8ddd-c474cc103d4e"
}
],
"failures": {},
"callbackResult": "Callback sent successfully"
}Example Request
Get Job Status
GETRetrieve the current processing status of a PDF conversion job, plus the latest accessibility score.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
caseId |
UUID | Unique identifier for the conversion job (returned by the upload endpoints) |
Job Status Values
queued— Job is waiting to be processedrunning— Job is currently being processedcompleted— Job completed successfullyfailed— Job failed during processingcanceled— Job was canceledquota_pending— Job is awaiting the asynchronous quota checkquota_exceeded— Job was rejected because the contract quota is exhausted or missing
Example Request
Example Response
{
"jobStatus": "completed",
"score": 95
}Download Converted PDF
GETDownload the converted PDF/UA file for a completed job. The response is the raw PDF bytes
(Content-Type: application/pdf). The pdf-version response header
contains the iteration number that was downloaded.
Example Request
Delete Case
DELETEPermanently delete a case and all of its files. Only the case owner (the user who owns the API key that uploaded it) can delete it.
Example Request
Webhooks
Webhooks deliver real-time notifications across the whole lifecycle of a conversion job —
not only on completion. When you supply webhookUrl (multipart upload) or
callbackUrl (URL upload) together with a secret, Accessful POSTs a signed,
versioned event to your endpoint each time the job changes state, including failures.
data.caseId) and the X-Accessful-Case-Id header. The body is a
versioned envelope and the recommended signature header is X-Accessful-Signature
(timestamped, replay-safe). The legacy X-Signature header is still sent during the
transition.
Event Types
Event (type) |
When it fires |
|---|---|
case.running |
Processing has started or advanced to the next stage |
case.completed |
The job finished successfully — the converted PDF is ready to download |
case.failed |
Processing failed and will not be retried |
case.quota_exceeded |
The job was rejected because the contract quota is exhausted or missing |
Webhook Request
Headers
| Header | Description |
|---|---|
Content-Type |
application/json |
X-Accessful-Signature |
Timestamped HMAC-SHA256, format t=<unix>,v1=<hex> — see
Signature Verification |
X-Accessful-Event-Id |
Stable id of the event — use it as an idempotency key to deduplicate retries |
X-Accessful-Event-Type |
The event type, e.g. case.completed |
X-Accessful-Case-Id |
The conversion job (case) id |
X-Accessful-Webhook-Timestamp |
Unix seconds when this attempt was signed (matches t=) |
X-Accessful-Delivery-Attempt |
1-based attempt counter for this delivery |
X-Signature |
Legacy — Base64 HMAC-SHA256 of the raw body. Still sent for backwards compatibility |
Payload
{
"id": "5b1f8c2e-9a3d-5e7b-8c4f-2d1a6b9e0f33",
"type": "case.completed",
"apiVersion": "2026-06-05",
"occurredAt": "2026-06-05T12:00:00Z",
"data": {
"caseId": "c8c956bb-19c3-451b-826f-5b3129eee4c1",
"fileName": "document.pdf",
"jobStatus": "completed"
}
}Signature Verification
Each delivery is signed with the secret you supplied at upload time. The
X-Accessful-Signature header has the form t=<unix>,v1=<hex>,
where v1 is the hex HMAC-SHA256 of "<t>.<raw-body>".
Recompute it, compare in constant time, and reject deliveries whose t is outside a
tolerance window (e.g. 5 minutes) to defeat replay.
Java
public boolean verify(String body, String sigHeader, String secret, long toleranceSeconds) throws Exception {
Map<String, String> parts = new HashMap<>();
for (String p : sigHeader.split(",")) { // t=...,v1=...
String[] kv = p.split("=", 2);
parts.put(kv[0], kv[1]);
}
long t = Long.parseLong(parts.get("t"));
if (Math.abs(Instant.now().getEpochSecond() - t) > toleranceSeconds) return false; // replay guard
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] raw = mac.doFinal((t + "." + body).getBytes(StandardCharsets.UTF_8));
String expected = HexFormat.of().formatHex(raw);
return MessageDigest.isEqual(expected.getBytes(), parts.get("v1").getBytes());
}Node.js
const crypto = require('crypto');
function verify(rawBody, sigHeader, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(sigHeader.split(',').map(p => p.split('=')));
const t = Number(parts.t);
if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false; // replay guard
const expected = crypto.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Retries & Idempotency
- Respond
2xxquickly to acknowledge. Any non-2xx, timeout or connection error is treated as a failed attempt. - Failed deliveries are retried with capped exponential backoff (up to 10 attempts over several hours); after that the delivery is marked exhausted.
- The same event may therefore arrive more than once. Deduplicate on
X-Accessful-Event-Id(equivalentlyidin the body) and keep your handler idempotent.
Errors
Errors are returned as RFC 7807 application/problem+json documents. Every error body
carries type, title, status and a human-readable
detail; some errors add extension fields (e.g. maxFileSize,
errorCode).
Error Body
{
"type": "https://accessful.de/problems/upload/payload-too-large",
"title": "Payload Too Large",
"status": 413,
"detail": "An uploaded file exceeds the maximum per-file size of 200MB.",
"maxFileSize": "200MB"
}Common Status Codes
| Status | Meaning |
|---|---|
400 Bad Request |
Malformed request, invalid parameter or URL |
401 Unauthorized |
Missing or invalid API key |
403 Forbidden |
The API key may not access this resource |
404 Not Found |
Unknown case id or resource |
409 Conflict |
The request conflicts with the current state of the case |
413 Payload Too Large |
A file or the request exceeds the upload limits |
429 Too Many Requests |
Rate limit hit — back off exponentially and retry |
500 Internal Server Error |
Unexpected server error — retry later |
Complete Examples
Quick Start with cURL
# Set your API key once (replace with the value your admin gave you) API_KEY="ak_your_api_key" BASE="https://api.accessful.de" # 1. Upload a PDF RESPONSE=$(curl -s -X POST "$BASE/api/v1/upload-service/pdf/upload" \ -H "X-API-Key: $API_KEY" \ -F "files=@document.pdf" \ -F "webhookUrl=https://your-server.com/callback" \ -F "secret=your-hmac-secret") # 2. Extract the case ID CASE_ID=$(echo "$RESPONSE" | jq -r '.successfulUploads[0]') # 3. (Optional) Poll for completion — prefer the webhook for production curl -s -X GET "$BASE/api/v1/upload-service/job-status/$CASE_ID" \ -H "X-API-Key: $API_KEY" # 4. Download the converted file curl -X GET "$BASE/api/v1/upload-service/download/$CASE_ID" \ -H "X-API-Key: $API_KEY" \ -o converted-document.pdf
JavaScript / Node.js
const FormData = require('form-data');
const fs = require('fs');
const axios = require('axios');
class AccessfulClient {
constructor(baseUrl, apiKey) {
this.baseUrl = baseUrl;
this.headers = { 'X-API-Key': apiKey };
}
async uploadPdf(filePath, webhookUrl = null, secret = null) {
const form = new FormData();
form.append('files', fs.createReadStream(filePath));
if (webhookUrl) form.append('webhookUrl', webhookUrl);
if (secret) form.append('secret', secret);
const response = await axios.post(
`${this.baseUrl}/api/v1/upload-service/pdf/upload`,
form,
{ headers: { ...form.getHeaders(), ...this.headers } }
);
return response.data;
}
async getJobStatus(caseId) {
const response = await axios.get(
`${this.baseUrl}/api/v1/upload-service/job-status/${caseId}`,
{ headers: this.headers }
);
return response.data;
}
async downloadPdf(caseId, outputPath) {
const response = await axios.get(
`${this.baseUrl}/api/v1/upload-service/download/${caseId}`,
{ headers: this.headers, responseType: 'stream' }
);
const writer = fs.createWriteStream(outputPath);
response.data.pipe(writer);
return new Promise((resolve, reject) => {
writer.on('finish', resolve);
writer.on('error', reject);
});
}
}
// Usage
const client = new AccessfulClient('https://api.accessful.de', 'ak_your_api_key');
(async () => {
const upload = await client.uploadPdf('./document.pdf');
const caseId = upload.successfulUploads[0];
console.log('Case ID:', caseId);
let status = { jobStatus: 'queued' };
while (status.jobStatus !== 'completed' && status.jobStatus !== 'failed') {
await new Promise(r => setTimeout(r, 5000));
status = await client.getJobStatus(caseId);
console.log('Status:', status);
}
if (status.jobStatus === 'completed') {
await client.downloadPdf(caseId, './converted-document.pdf');
console.log('Download complete.');
}
})();Spring Boot — minimal configuration
# application.properties accessful.api.base-url=https://api.accessful.de accessful.api.key=ak_your_api_key callback.hmac.secret=your-hmac-secret callback.url=https://your-server.com/api/callback/
Inject the key as a header on every outgoing request — no token-refresh logic required:
@Bean
public RestClient accessfulRestClient(
@Value("${accessful.api.base-url}") String baseUrl,
@Value("${accessful.api.key}") String apiKey) {
return RestClient.builder()
.baseUrl(baseUrl)
.defaultHeader("X-API-Key", apiKey)
.build();
}Limits, Rate Limits & Best Practices
Upload Limits
| Limit | Value |
|---|---|
| Maximum size per file | 200 MB |
| Maximum total request size | 815 MB |
| Maximum files per upload | 1000 |
| Pages per document | No fixed per-document cap — bounded by your contract quota. Exceeding the quota yields
a quota_exceeded job status. |
Exceeding a size or file-count limit returns 413 Payload Too Large with the exact
limits in the error body (see Errors).
Rate Limits
- Upload requests are rate-limited per organisation and per API key; the exact thresholds and allowed concurrency depend on your plan — contact support for your numbers.
- HTTP
429means you hit the limit — back off exponentially and retry.
Best Practices
- Use webhooks instead of polling — cheaper and faster.
- Verify HMAC on every incoming webhook.
- Check
duplicateFilesin upload responses to avoid re-processing. - Delete cases when you no longer need them to keep your storage footprint small.
- Store the API key in a secret manager, not in source control.
- Rotate keys on a fixed cadence and immediately on personnel changes. Generate the new key, deploy, then revoke the old one.