Quickstart
Upload a PDF. Get a useful result. Start with a free validation.
1. Create a key
Open API setup, choose a name and copy your key. An organization owner or administrator can create keys when the organization’s purchased plan includes API access.
Keep the key on your server or in your local environment. Never embed it in a browser application.
export PORTER_API_KEY="your-key"2. Run an example
Python needs requests; JavaScript needs Node.js 22 or later. The shell example needs Bash, cURL, jq and uuidgen. Pass a PDF path when running the example. Use python porter.py document.pdf for Python.
The example prepares an upload, sends the PDF directly to storage, creates a validation job, and waits for its report. Validation-only uses zero page credits. The same API key works across your organization’s API-created files.
# pip install requests
# Set PORTER_API_KEY in your environment. Save as porter.py.
import os, sys, time, uuid
from pathlib import Path
import requests
base = "https://api.autotune.porter.fi/v1"
path = Path(sys.argv[1])
headers = {"Authorization": "Bearer " + os.environ["PORTER_API_KEY"]}
def api(method, route, body=None):
request_headers = dict(headers)
if method == "POST":
request_headers["Idempotency-Key"] = str(uuid.uuid4())
for attempt in range(6):
try:
response = requests.request(method, base + route, json=body,
headers=request_headers, timeout=30)
except requests.RequestException:
if attempt == 5: raise
time.sleep(min(2 ** attempt, 30))
continue
data = response.json()
retry = response.status_code == 429 or response.status_code >= 500 or (
response.status_code == 409 and data.get("error", {}).get("code") == "request_in_progress")
if retry and attempt < 5:
time.sleep(min(int(response.headers.get("Retry-After", 2 ** attempt)), 60))
continue
response.raise_for_status()
return data
upload = api("POST", "/uploads", {"filename": path.name, "size_bytes": path.stat().st_size})
with path.open("rb") as pdf:
sent = requests.post(upload["upload_url"], data=upload["fields"],
files={"file": (path.name, pdf, "application/pdf")}, timeout=300)
sent.raise_for_status()
job = api("POST", "/jobs", {"upload_id": upload["upload_id"], "mode": "validate_only"})
for _ in range(360):
job = api("GET", "/jobs/" + job["id"])
if job["status"] in ("completed", "failed"): break
time.sleep(5)
else:
raise TimeoutError("Keep job id " + job["id"] + " and check it later")
print(job)
if job["status"] == "failed":
raise RuntimeError(job["error"])
if job["validation"]["status"] != "skipped":
print(api("GET", "/jobs/" + job["id"] + "/report"))
if job["has_output"]:
download = api("GET", "/jobs/" + job["id"] + "/download")
pdf = requests.get(download["download_url"], timeout=300)
pdf.raise_for_status()
Path("repaired.pdf").write_bytes(pdf.content)
3. Read the result
completed means the requested work finished. Check the report for accessibility findings; completion alone does not establish conformance. Your job also appears in API setup.
For automatic notifications, follow the webhook guide. To repair PDFs, select a repair mode.