API reference
Upload a PDF, start a job, then retrieve that job’s report and output. Each job identifies one exact run.
Base URL: https://api.autotune.porter.fi/v1
Send Authorization: Bearer $PORTER_API_KEY on every API request. POST requests also need Content-Type: application/json and an Idempotency-Key header (1–200 characters). Choose a new key for each operation; reuse it with the same body when retrying that operation. Reusing a key with a different body returns HTTP 409. Unknown request fields are rejected.
GET and DELETE requests have no body. Responses below are JSON; signed storage URLs and values are illustrative. See the OpenAPI schema for the full typed contract.
GET /account
Check your API key and current account limits. No request body or page credits are needed.
Response · 200 OK
{
"api_access_enabled": true,
"max_upload_size_bytes": 209715200,
"concurrent_documents": 2,
"requests_per_minute": 600,
"uploads_per_minute": 60
}| Field | Meaning |
|---|---|
api_access_enabled | Whether the account’s plan includes API repair. Validation-only remains available without this entitlement. |
max_upload_size_bytes | Maximum PDF size in bytes. Check this before uploading. |
concurrent_documents | Number of PDFs your account can process at once. Additional work queues. |
requests_per_minute | General API request limit per minute, shared by the organization’s keys. |
uploads_per_minute | Separate limit on upload creation per minute. |
These values are examples; use your account’s response. Chat with us about higher limits.
POST /uploads
Reserve an upload before sending the PDF. This step does not start processing.
Request
{
"filename": "document.pdf",
"size_bytes": 12345
}| Field | Meaning |
|---|---|
filename | Required string, 1–255 characters, ending in .pdf. |
size_bytes | Required positive integer. The exact size of the PDF bytes you will upload, within your account’s limit. |
Response · 201 Created
{
"upload_id": "7b1f66cc-45ef-4f0a-9c37-0bc35c012001",
"upload_url": "https://storage.example.com/",
"fields": {
"key": "example/upload/document.pdf",
"Content-Type": "application/pdf",
"x-amz-meta-upload-id": "7b1f66cc-45ef-4f0a-9c37-0bc35c012001",
"x-amz-meta-expected-size": "12345",
"x-amz-meta-filename-sha256": "<returned digest>",
"x-amz-meta-owner-sha256": "<returned digest>",
"x-amz-meta-resource-id-sha256": "<returned digest>",
"x-amz-meta-completion-token-sha256": "<returned digest>",
"x-amz-algorithm": "AWS4-HMAC-SHA256",
"x-amz-credential": "<returned credential>",
"x-amz-date": "20261021T090000Z",
"x-amz-security-token": "<returned token>",
"policy": "<returned policy>",
"x-amz-signature": "<returned signature>"
},
"expires_in_seconds": 900
}Save upload_id for the job request. upload_url is the storage destination; fields is a map of form-field names to string values. Copy every returned field unchanged, including any fields not shown here. Do not construct or interpret the signing fields. expires_in_seconds is the upload URL’s lifetime from issuance, not the PDF’s retention period.
- POST
multipart/form-datatoupload_url, with all returnedfieldsand a finalfilepart containing the PDF bytes. - Let your HTTP library set the multipart boundary. Do not send your Porter API key to storage.
- Wait for storage to return a successful 2xx response, then create the job. Storage may return an empty body; do not expect JSON.
The quickstart includes complete upload code in Python, JavaScript, and cURL.
POST /jobs
Start work on a completed upload. Use a different idempotency key from the upload request.
Request
{
"upload_id": "7b1f66cc-45ef-4f0a-9c37-0bc35c012001",
"mode": "fix_and_validate",
"reference": "invoice-1042",
"retention": "14",
"automatic_improvements": false
}| Field | Meaning |
|---|---|
upload_id | Required string returned by POST /uploads, after its PDF has reached storage. |
mode | Optional. fix_and_validate repairs and validates (default); fix_only repairs without accessibility validation; validate_only checks the original without repairing or using page credits. |
reference | Optional string, up to 200 characters, or null. Your own identifier, returned unchanged in jobs and events. It does not deduplicate requests. |
retention | Optional string: "14" (default), "90", or "keep". Retain the file for that many days, or until you delete it. Use strings, not numbers. |
automatic_improvements | Optional boolean, false by default. Opt this file into later improvement runs. Requires fix_and_validate and an eligible plan. Improvements may arrive days or weeks later, while the file is retained. |
Response · 202 Accepted
{
"id": "7b1f66cc-45ef-4f0a-9c37-0bc35c012003",
"file_id": "7b1f66cc-45ef-4f0a-9c37-0bc35c012002",
"reference": "invoice-1042",
"mode": "fix_and_validate",
"status": "queued",
"trigger": "initial",
"previous_job_id": null,
"page_count": 3,
"has_output": false,
"validation": {
"status": "pending",
"failed_checks": null,
"failed_rules": null
},
"error": null,
"created_at": "2026-10-21T09:00:00Z",
"expires_at": "2026-11-04T09:00:00Z"
}Accepted means the job exists, not that it has finished. Save id to poll or retrieve its results, and file_id to identify or delete the underlying PDF. An upload belongs to one job request; use a new upload for new work. Retrying the same request returns the existing job, which may already have progressed.
GET /jobs/{job_id}
Replace {job_id} with the job’s id. No request body. Poll every five seconds, or use webhooks.
Response · 200 OK
{
"id": "7b1f66cc-45ef-4f0a-9c37-0bc35c012003",
"file_id": "7b1f66cc-45ef-4f0a-9c37-0bc35c012002",
"reference": "invoice-1042",
"mode": "fix_and_validate",
"status": "completed",
"trigger": "initial",
"previous_job_id": null,
"page_count": 3,
"has_output": true,
"validation": {
"status": "completed",
"failed_checks": 0,
"failed_rules": 0
},
"error": null,
"created_at": "2026-10-21T09:00:00Z",
"expires_at": "2026-11-04T09:00:00Z"
}Job fields
| Field | Meaning |
|---|---|
id / file_id | UUID strings for this run and its PDF. Later runs get a new job ID and keep the same file ID. |
reference / mode | Your supplied reference (or null) and the processing mode for this run. |
status | Overall job state; see the states below. Stop polling at completed or failed. |
trigger | initial, manual_retry, or automatic_improvement: why this run was created. |
previous_job_id | Prior job’s UUID when linked, otherwise null. Retrieve either job by its own ID to keep results distinct. |
page_count | Integer page count, or null while unavailable. |
has_output | Whether a repaired PDF is available for this run. Always false for validation-only. This is not a validation verdict. |
validation | Accessibility check status and counts. Describes the original for validation-only, or the repaired output for regular processing. |
error | Null unless the job failed; otherwise an object with machine-readable code and human-readable message. Accessibility findings alone do not make a job fail. |
created_at / expires_at | UTC ISO 8601 timestamps for job creation and file expiry. Expiry is null for indefinite retention. A later run does not imply a new retention period. |
Job states
| Status | Meaning |
|---|---|
queued | Waiting to start. |
processing | Processing is underway. |
validating | Waiting for accessibility validation to finish. |
completed | All requested stages finished. Inspect validation counts; completion does not mean all checks passed. |
failed | Processing or validation could not finish. Read error. A repaired PDF may still be available if validation failed; check has_output. |
validation.status is pending, running, completed, failed, or skipped. failed_checks counts individual non-warning failures; failed_rules counts distinct failing rules. Null means a count is unavailable, not zero. For fix_only, validation is skipped and both counts are null.
An automatic improvement creates a separate job, potentially weeks later. The original completed job stays completed. Subscribe to events to learn about later runs; do not keep polling the original job for a replacement.
GET /jobs/{job_id}/report
Get accessibility findings for this exact run. No request body.
Response · 200 OK
This example shows a validation-only run with one failing rule. Rule text is illustrative; optional rule metadata and null location fields are omitted for readability.
{
"ticket_id": "7b1f66cc-45ef-4f0a-9c37-0bc35c012004",
"files": [
{
"file_id": "7b1f66cc-45ef-4f0a-9c37-0bc35c012002",
"before": {
"passedRules": 0,
"failedRules": 1,
"warningRules": 0,
"passedChecks": 2,
"failedChecks": 1,
"warningChecks": 0,
"errorMessage": null,
"categories": [
{
"title": "Alternative text",
"description": "Text alternatives for figures.",
"tags": [],
"passedRules": 0,
"failedRules": 1,
"warningRules": 0,
"passedChecks": 2,
"failedChecks": 1,
"warningChecks": 0,
"rules": [
{
"description": "A figure needs a text alternative.",
"passed": false,
"isWarning": false,
"passedChecks": 2,
"failedChecks": 1,
"instances": [
{
"errorMessage": "Missing text alternative.",
"pageIndex": 0,
"pageNumber": 1,
"objectType": "Figure"
}
]
}
]
}
]
},
"after": null
}
],
"deep_dive_enabled": true
}Report structure
| Path | Meaning |
|---|---|
ticket_id | Report grouping identifier. Continue using the job ID for API requests. |
files[] | Per-file results; match by file_id. A developer job processes one PDF. |
files[].before / after | Validation snapshots of the original and repaired PDF. Validation-only returns before with after: null. Regular processing includes both when available. A null snapshot means unavailable, not passed. |
passedRules, failedRules, warningRules | Summary counts of rules in each outcome, at snapshot and category levels. Warning rules are excluded from failed-rule totals. |
passedChecks, failedChecks, warningChecks | Counts of individual checks. One rule can test many objects and produce multiple failures. Warning checks are excluded from failed-check totals. |
categories[] | Groups of related rules, with a title, description, tags, summary counts, and a rules array. |
rules[] | Each rule has passed, isWarning, check counts, and instances. Optional metadata includes ruleKey, ruleId, description, specification, clause, testNumber, tags, note, and githubClauseLink. |
instances[] | Up to 20 locations per rule, with an errorMessage and available context. Counts describe the full report and may exceed the number of returned instances. |
pageIndex / pageNumber | Zero-based index and one-based page number. Either may be null when the checker cannot locate the finding. |
bboxPct | Optional instance bounding box: left, right, bottom, top, each from 0 to 1. Other optional location fields include context, objectType, fontName, mcid, annotSubtype, outlineTitle, and outlineDestPageIndex. |
errorMessage | At snapshot level, a validation execution error or null. When set, zero or missing counts must not be interpreted as a successful check. |
deep_dive_enabled | Whether rule-instance details are included. Enabled for developer API reports. |
Validation snapshots use camelCase field names; job fields use snake_case. HTTP 409 with report_not_ready means retry after the Retry-After interval. validation_skipped means this job used fix_onlyand will not produce a report. A failed validation may return partial findings; check the job status and snapshot errors.
GET /jobs/{job_id}/download
Request a temporary download link for this run’s repaired PDF. No request body.
Response · 200 OK
{
"file_id": "7b1f66cc-45ef-4f0a-9c37-0bc35c012002",
"download_url": "https://storage.example.com/document.pdf?signature=example",
"expires_in_seconds": 300
}file_id identifies the PDF. GET download_url without your Porter API key to receive PDF bytes, not JSON. expires_in_seconds is the link’s lifetime from issuance (at most five minutes). Request a fresh link when needed, while the file is retained. The result belongs to the requested job, even if a later run exists.
HTTP 409 with output_unavailable means this job has no repaired PDF available, including validation-only jobs. Check has_output before requesting a link.
DELETE /files/{file_id}
Use the job’s file_id, not its id. No request body.
Response · 202 Accepted
{
"file_id": "7b1f66cc-45ef-4f0a-9c37-0bc35c012002",
"deleted": true
}deleted: true means the file is marked deleted and new artifact access is blocked. Storage cleanup runs asynchronously and covers retained artifacts across runs. Already issued download links may remain usable until their short expiry. Billing and run history remain. Repeated deletion is safe.
Error responses
Non-2xx API responses use an error object. For example, a report requested too early returns HTTP 409 with Retry-After: 5:
{
"error": {
"code": "report_not_ready",
"message": "The validation report is not ready."
},
"request_id": "7b1f66cc-45ef-4f0a-9c37-0bc35c012005"
}Branch on error.code; use error.message for a readable explanation. request_id also appears in the X-Request-ID response header and helps us investigate a request. HTTP 422 field-validation errors can include error.fields, an array of {"path": "body.size_bytes", "message": "…"} objects. See Errors & retries for retry behavior and other status codes.