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 canceled
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
DELETEDelete a case and its associated 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 when a PDF finishes processing. When you supply
webhookUrl (multipart upload) or callbackUrl (URL upload) along with a
secret, Accessful POSTs the result to your endpoint and signs the payload with HMAC-SHA256.
Webhook Request
Headers
| Header | Description |
|---|---|
Content-Type |
application/json |
X-Signature |
Base64-encoded HMAC-SHA256 of the raw request body, using the secret you supplied at upload time |
Payload
{
"jobStatus": "completed",
"caseId": "c8c956bb-19c3-451b-826f-5b3129eee4c1",
"fileName": "document.pdf"
}HMAC Verification
Compute the HMAC-SHA256 of the raw request body using your secret, base64-encode it, and compare
it to the X-Signature header using a constant-time comparison.
Java
public boolean verifyHmac(String payload, String signature, String secret) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] rawHmac = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
String expected = Base64.getEncoder().encodeToString(rawHmac);
return MessageDigest.isEqual(expected.getBytes(), signature.getBytes());
} catch (Exception e) {
return false;
}
}Node.js
const crypto = require('crypto');
function verifyHmac(payload, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(payload);
const expected = hmac.digest('base64');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}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();
}Rate Limits & Best Practices
Rate Limits
- Upload requests are limited per organisation and per API key.
- 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.