Accessful API

PDF to PDF/UA Conversion Service

REST API API Key Webhooks PDF/UA v3.1.0

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.

Base URL: https://api.accessful.de
API Version: 2.1.0
Authentication: X-API-Key header
Content Types: application/json, multipart/form-data

Key Features

API Workflow

1. Send X-API-Key
2. Upload PDF(s)
3. Process
4. Webhook / Poll
5. Download

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.

Privacy: Your API key stays in this browser tab. It's sent only to 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

Max 100 MB. PDF only.

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/…
Important: The old realm 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.

Header: 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:

curl -i "https://api.accessful.de/api/v1/upload-service/job-status/00000000-0000-0000-0000-000000000000" \ -H "X-API-Key: ak_your_api_key"

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?

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)

POST
/api/v1/upload-service/pdf/upload

Upload 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

200 Upload accepted, files queued for analysis
400 Invalid file type or malformed request
401 API key missing or invalid
429 Upload limit exceeded
500 Internal server error

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

curl -X POST "https://api.accessful.de/api/v1/upload-service/pdf/upload" \ -H "X-API-Key: ak_your_api_key" \ -F "files=@document1.pdf" \ -F "files=@document2.pdf" \ -F "webhookUrl=https://your-server.com/callback" \ -F "secret=your-hmac-secret" \ -F "folder-name=Marketing"

Upload PDF Files by URL

POST
/api/v1/upload-service/pdf/upload-by-url-list

Upload 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

202 Upload accepted for processing
400 Invalid URL format or filename
401 API key missing or invalid
429 Upload limit exceeded
500 Internal server error

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

curl -X POST "https://api.accessful.de/api/v1/upload-service/pdf/upload-by-url-list" \ -H "X-API-Key: ak_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "files": [ { "url": "https://example.com/document1.pdf", "filename": "document1.pdf" }, { "url": "https://example.com/document2.pdf", "filename": "document2.pdf" } ], "callbackUrl": "https://your-server.com/callback", "hmacSignature": "your-hmac-secret" }'

Get Job Status

GET
/api/v1/upload-service/job-status/{caseId}

Retrieve 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 processed
  • running — Job is currently being processed
  • completed — Job completed successfully
  • failed — Job failed during processing
  • canceled — Job was canceled
  • quota_pending — Job is awaiting the asynchronous quota check
  • quota_exceeded — Job was rejected because the contract quota is exhausted or missing

Example Request

curl -X GET "https://api.accessful.de/api/v1/upload-service/job-status/c8c956bb-19c3-451b-826f-5b3129eee4c1" \ -H "X-API-Key: ak_your_api_key"

Example Response

{
  "jobStatus": "completed",
  "score": 95
}

Download Converted PDF

GET
/api/v1/upload-service/download/{caseId}

Download 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

curl -X GET "https://api.accessful.de/api/v1/upload-service/download/c8c956bb-19c3-451b-826f-5b3129eee4c1" \ -H "X-API-Key: ak_your_api_key" \ -o converted-document.pdf

Delete Case

DELETE
/api/v1/upload-service/delete/{caseId}

Permanently 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.

Irreversible. The stored PDFs (including every iteration) are hard-deleted and the underlying file objects are purged — there is no soft-delete and no recovery.
Retention. Cases and their files are kept until you delete them — there is no automatic expiry. Delete cases you no longer need to keep your storage footprint small.

Example Request

curl -X DELETE "https://api.accessful.de/api/v1/upload-service/delete/c8c956bb-19c3-451b-826f-5b3129eee4c1" \ -H "X-API-Key: ak_your_api_key"

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.

Breaking change (v3.0.0): events are POSTed to your exact registered URL — the case id is no longer appended to the path. It is now in the payload (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

POST
https://your-server.com/your/exact/callback

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));
}
Verify on the raw body. Compute the HMAC over the exact bytes you received, before any JSON re-serialisation — re-encoding can change whitespace or key order and break the signature.

Retries & Idempotency

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

Best Practices