Docs · updated 31 October 2026
Quickstart and reference
Base URL https://api.pdftrusted.com. JSON in, JSON out. Every tool uses the same four calls.
Quickstart
- Sign in with Google or email, then create a key in the console. The secret is shown once.
- Create a task, upload the file, process it and download the result.
cURL
API=https://api.pdftrusted.com
ID=$(curl -sS -X POST $API/v1/tasks -H "X-API-Key: $KEY" \
-H "Content-Type: application/json" -d '{"tool":"compress"}' | jq -r .task_id)
curl -sS -X PUT $API/v1/tasks/$ID/upload -H "X-API-Key: $KEY" -F file=@in.pdf
curl -sS -X POST $API/v1/tasks/$ID/process -H "X-API-Key: $KEY" # waits; 200 when done
curl -sS -L "$API/v1/tasks/$ID/download?redirect=1" -H "X-API-Key: $KEY" -o out.pdfPython
import os, requests
API, H = "https://api.pdftrusted.com", {"X-API-Key": os.environ["KEY"]}
t = requests.post(f"{API}/v1/tasks", headers=H, json={"tool": "compress"}).json()
requests.put(f"{API}/v1/tasks/{t['task_id']}/upload", headers=H, files={"file": open("in.pdf", "rb")}).raise_for_status()
r = requests.post(f"{API}/v1/tasks/{t['task_id']}/process?async=1", headers=H).json()
while r["status"] == "processing":
r = requests.get(f"{API}/v1/tasks/{t['task_id']}?wait=15", headers=H).json()
if r["status"] == "done":
url = requests.get(f"{API}/v1/tasks/{t['task_id']}/download", headers=H).json()["download_url"]
open("out.pdf", "wb").write(requests.get(url).content)
else:
print(r["error"], r.get("error_message"))Node.js
const API = "https://api.pdftrusted.com";
const H = { "X-API-Key": process.env.KEY };
const task = await (await fetch(`${API}/v1/tasks`, {
method: "POST", headers: { ...H, "Content-Type": "application/json" },
body: JSON.stringify({ tool: "compress" }),
})).json();
const form = new FormData();
form.append("file", new Blob([fs.readFileSync("in.pdf")]), "in.pdf");
await fetch(`${API}/v1/tasks/${task.task_id}/upload`, { method: "PUT", headers: H, body: form });
await fetch(`${API}/v1/tasks/${task.task_id}/process`, { method: "POST", headers: H });
const { download_url } = await (await fetch(`${API}/v1/tasks/${task.task_id}/download`, { headers: H })).json();Authentication and keys
Send your key as X-API-Key: sk_... (or Authorization: Bearer sk_...). Never put a key in browser or mobile code; call the API from your server.
- Main key: your account's plan and quota.
- Extra live keys (
sk_live_): up to 10, one per app or environment, same plan and quota. Delete one without touching the others. - Test keys (
sk_test_): a free sandbox, 100 jobs a day, 5 MB and 10 pages a file, every tool except AI ones. Never touches your quota or credits.
Keys are managed in the console or over the API with your sign-in token (a key can't manage keys): GET /v1/keys, POST /v1/keys, POST /v1/keys/rotate (new secret, same quota), DELETE /v1/keys, POST /v1/keys/extra with {"name","mode"} and DELETE /v1/keys/extra/{id}.
Tasks
GET/v1/tools
GET /v1/tools/{tool} gives its upload field (file, repeated files or none), a required option and the size, page and file caps per plan. Browse them on the tools page.POST/v1/tasks
tool, optional options. Answers 201 with task_id and upload_url. For big files send filename, size and content_type (or files for several) to get a signed URL, so the file goes straight to storage. A file over your cap is refused here, before any upload (413).PUT/v1/tasks/{id}/upload
file (files, repeated, for merge and image to PDF). Or PUT the bytes to the signed URL with the headers from the create answer.POST/v1/tasks/{id}/process
download_url. With ?async=1 (or Prefer: respond-async) it answers 202 at once; a job still running after 90 seconds also turns into 202. Calling it again never starts a second job.GET/v1/tasks/{id}
status: created, uploaded, processing, done, failed or expired. Add ?wait=15 to hold the answer up to 15 s until the job finishes.GET/v1/tasks/{id}/download
{"download_url": "..."}, a signed link valid for 15 minutes. ?redirect=1 gives a 302 instead (handy with curl -L).DELETE/v1/tasks/{id}
not_found, or 409 task_busy while processing. Otherwise files are removed within an hour of the job finishing, and an upload that is never processed expires after an hour (410 upload_expired).Webhooks
Instead of polling, POST /v1/tasks/{id}/process with JSON {"webhook_url": "https://you.example/hook"}. It answers 202, and we POST task.completed or task.failed with the task as JSON. HTTPS only, public addresses only, port 443 or 8443. We try 3 times (now, after 5 s, after 30 s) and stop on a 4xx other than 408, 409, 425 or 429. GET /v1/tasks/{id} shows the delivery status.
Verify PDFTrusted-Signature: t=...,v1=...: HMAC-SHA256 with your webhook secret (console → Settings) over t + "." + raw body. Reject a t more than 5 minutes old.
import hmac, hashlib, time
def verify(secret, header, body: bytes):
p = dict(x.split("=", 1) for x in header.split(","))
mac = hmac.new(secret.encode(), p["t"].encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, p["v1"]) and abs(time.time() - int(p["t"])) < 300Credits and usage
Free accounts get 50 jobs a month (10 a minute, one at a time, 25 MB and 50 pages a file). On an API plan each successful job spends credits from the per-tool table: taken when the job starts, given back if it fails. Per-page tools count the pages of the file you send.
GET/v1/me/usage
plan, used, remaining, caps, and billing.meters with used and cap for api_credits, ocr_pages, translate_pages and engine_s. Add ?refresh=1 right after a purchase.GET/v1/me/jobs?days=30
Errors and limits
Every error is {"error": {"code", "message"}}. Branch on code, show message. Limit errors also carry upgrade_url or credits_url, and docs_url. Every 429 and 503 has Retry-After; task creation and usage send X-RateLimit-Limit, -Remaining and -Reset. Every answer has X-Request-Id; quote it to support.
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 429 | api_credits_exceeded | Not enough API credits left this month for this job (needed and left are in the error). | Upgrade in the console, or wait for the monthly reset. |
| 429 | quota_exceeded | The plan's job quota for this day or month is used. | Wait for Retry-After, or upgrade (upgrade_url). |
| 429 | ocr_pages_exceeded | This month's OCR pages are used. | Upgrade your API plan (credits_url). |
| 429 | translate_pages_exceeded | This month's translated pages are used. | Upgrade your API plan (credits_url). |
| 429 | engine_time_exceeded | This month's processing time is used. | Wait for the next month, or pick a larger plan. |
| 429 | too_many_concurrent | Too many jobs at once for the plan. | Wait for a running job to finish. |
| 429 | rate_limited | Too many jobs this minute. | Retry after Retry-After seconds. |
| 413 | file_too_large | The file is over the plan's size cap. | Split or compress it, or upgrade (upgrade_url). |
| 413 | too_many_pages | The file has more pages than the plan allows. | Split it, or upgrade (upgrade_url). |
| 401 | invalid_api_key | The key is wrong, deleted or turned off. | Check the key, or rotate it in the console. |
| 422 | pdf_password | The PDF is encrypted. | Send the password in options, or unlock it first. |
| 503 | engine_unavailable | The engine is busy or restarting. | Retry after Retry-After seconds. |
Other codes you may see: 400 unknown_tool, bad_request, bad_webhook_url; 413 too_many_files; 422 not_pdf, pdf_unreadable; 403 test_key_tool (AI tool with a test key).
OpenAPI
The machine-readable spec is at api.pdftrusted.com/openapi.json, with an interactive reference at api.pdftrusted.com/docs. Import it into Postman or generate a client.