Skip to content
Porter

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
}
FieldMeaning
api_access_enabledWhether the account’s plan includes API repair. Validation-only remains available without this entitlement.
max_upload_size_bytesMaximum PDF size in bytes. Check this before uploading.
concurrent_documentsNumber of PDFs your account can process at once. Additional work queues.
requests_per_minuteGeneral API request limit per minute, shared by the organization’s keys.
uploads_per_minuteSeparate 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
}
FieldMeaning
filenameRequired string, 1–255 characters, ending in .pdf.
size_bytesRequired 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.

  1. POST multipart/form-data to upload_url, with all returned fields and a final file part containing the PDF bytes.
  2. Let your HTTP library set the multipart boundary. Do not send your Porter API key to storage.
  3. 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
}
FieldMeaning
upload_idRequired string returned by POST /uploads, after its PDF has reached storage.
modeOptional. fix_and_validate repairs and validates (default); fix_only repairs without accessibility validation; validate_only checks the original without repairing or using page credits.
referenceOptional string, up to 200 characters, or null. Your own identifier, returned unchanged in jobs and events. It does not deduplicate requests.
retentionOptional string: "14" (default), "90", or "keep". Retain the file for that many days, or until you delete it. Use strings, not numbers.
automatic_improvementsOptional 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

FieldMeaning
id / file_idUUID strings for this run and its PDF. Later runs get a new job ID and keep the same file ID.
reference / modeYour supplied reference (or null) and the processing mode for this run.
statusOverall job state; see the states below. Stop polling at completed or failed.
triggerinitial, manual_retry, or automatic_improvement: why this run was created.
previous_job_idPrior job’s UUID when linked, otherwise null. Retrieve either job by its own ID to keep results distinct.
page_countInteger page count, or null while unavailable.
has_outputWhether a repaired PDF is available for this run. Always false for validation-only. This is not a validation verdict.
validationAccessibility check status and counts. Describes the original for validation-only, or the repaired output for regular processing.
errorNull 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_atUTC 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

StatusMeaning
queuedWaiting to start.
processingProcessing is underway.
validatingWaiting for accessibility validation to finish.
completedAll requested stages finished. Inspect validation counts; completion does not mean all checks passed.
failedProcessing 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

PathMeaning
ticket_idReport 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 / afterValidation 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, warningRulesSummary counts of rules in each outcome, at snapshot and category levels. Warning rules are excluded from failed-rule totals.
passedChecks, failedChecks, warningChecksCounts 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 / pageNumberZero-based index and one-based page number. Either may be null when the checker cannot locate the finding.
bboxPctOptional 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.
errorMessageAt snapshot level, a validation execution error or null. When set, zero or missing counts must not be interpreted as a successful check.
deep_dive_enabledWhether 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.