Skip to content

PDFTrusted API

Convert, compress, OCR and edit PDFs from your own code with the same engine that runs pdftrusted.com.

Every job is four calls:

  1. POST /v1/tasks with the tool name (and the file size for a direct upload link).
  2. Upload the file: PUT to signed_upload_url, or multipart to /v1/tasks/{id}/upload.
  3. POST /v1/tasks/{id}/process (add ?async=1 for long jobs, then poll GET /v1/tasks/{id}).
  4. GET /v1/tasks/{id}/download.

Sign in with Authorization: Bearer <access token>, or use an API key as X-API-Key: <key> (Authorization: Bearer sk_... works too). Without either you are a guest with small daily limits. Get a key at https://developers.pdftrusted.com/console/keys (signed in). GET /v1/keys shows it (last 4 characters) and your webhook secret, POST /v1/keys/rotate replaces a lost or leaked key at once (same quota), DELETE /v1/keys turns it off.

Errors are always {"error": {"code", "message"}} with a stable code. Every answer has X-Request-Id (quote it to support). Limits: X-RateLimit-Limit / -Remaining / -Reset on task creation and usage; every 429/503 has Retry-After (seconds).

Webhooks: POST /v1/tasks/{id}/process?async=1 with JSON {"webhook_url": "https://you.example/hook"} and we POST {"event": "task.completed" | "task.failed", "task": {...}} there when it finishes, signed with PDFTrusted-Signature: t=<unix>,v1=<hex HMAC-SHA256(webhook_secret, "<t>.<raw body>")>.

Files are deleted automatically 1 hour after a job finishes (or now with DELETE /v1/tasks/{id}).

Guides and examples: https://developers.pdftrusted.com

Tasks

Create, upload, process, poll, download and delete a job.

post/v1/tasksCreate a task

Body: application/json (TaskIn)

  • 201 Created
  • 400 Error: `bad_dpi`, `bad_fmt`, `bad_image`, `bad_lang`, `bad_level`, `bad_mode`, `bad_option`, `bad_page`, `bad_pages`, `bad_paper`, `bad_request`, `bad_sep`, `bad_size`, `bad_target`, `bad_typing`, `bad_url`, `bad_webhook_url`, `batch_empty`, `empty`, `empty_file`, `engine_error`, `no_url`, `not_pptx`, `not_xlsx`, `unknown_tool`, `webhooks_disabled`
  • 401 Error: `invalid_api_key`, `invalid_token`, `sign_in_required`, `token_expired`, `unauthorized`
  • 403 Error: `ai_not_in_free_api`, `blocked_url`, `custom_plan_required`, `disabled`, `forbidden`, `test_key_tool`
  • 413 Error: `engine_error`, `batch_too_large`, `decompression_limit`, `docx_too_large`, `file_too_large`, `output_too_large`, `too_complex`, `too_large`, `too_many_files`, `too_many_pages`
  • 422 Error: `bad_image`, `engine_error`, `not_docx`, `bad_pdf`, `cannot_compress`, `cannot_convert`, `cannot_fetch`, `docx_password`, `docx_unreadable`, `hwp_distribution`, `hwp_password`, `no_text`, `nothing_found`, `page_count_unknown`, `pdf_empty`, `pdf_password`, `pdf_unreadable`, `scanned_pdf`, `unreadable`
  • 429 Error: `api_credits_exceeded`, `engine_time_exceeded`, `free_translate_pages_exceeded`, `ip_quota_exceeded`, `ocr_pages_exceeded`, `output_quota_exceeded`, `quota_exceeded`, `rate_limited`, `too_many_concurrent`, `too_many_open_tasks`, `translate_pages_exceeded`
  • 503 Error: `ai_off`, `auth_unavailable`, `busy`, `engine_unavailable`, `interrupted`, `storage_unavailable`, `unavailable`
put/v1/tasks/{tid}/uploadUpload the file(s) (multipart)

Multipart field 'file'; a multi-file tool (img2pdf) takes 'files', repeated (a single 'file' also works).

ParameterInTypeDescription
tid*pathstringtask_id

Body: multipart/form-data (Body_upload_v1_tasks__tid__upload_put)

  • 200 Uploaded
  • 400 Error: `bad_dpi`, `bad_fmt`, `bad_image`, `bad_lang`, `bad_level`, `bad_mode`, `bad_option`, `bad_page`, `bad_pages`, `bad_paper`, `bad_request`, `bad_sep`, `bad_size`, `bad_target`, `bad_typing`, `bad_url`, `bad_webhook_url`, `batch_empty`, `empty`, `empty_file`, `engine_error`, `no_url`, `not_pptx`, `not_xlsx`, `unknown_tool`, `webhooks_disabled`
  • 401 Error: `invalid_api_key`, `invalid_token`, `sign_in_required`, `token_expired`, `unauthorized`
  • 404 Error: `unknown_tool`, `no_key`, `not_found`
  • 409 Error: `bad_state`, `not_ready`, `task_busy`, `too_many_keys`, `trial_key_exists`
  • 410 Error: `upload_expired`
  • 413 Error: `engine_error`, `batch_too_large`, `decompression_limit`, `docx_too_large`, `file_too_large`, `output_too_large`, `too_complex`, `too_large`, `too_many_files`, `too_many_pages`
  • 415 Error: `engine_error`, `legacy_doc`, `not_docx`, `not_epub`, `not_html`, `not_hwp`, `not_markdown`, `not_pdf`, `not_zip`, `unsupported_input`, `unsupported_media_type`
  • 422 Error: `bad_image`, `engine_error`, `not_docx`, `bad_pdf`, `cannot_compress`, `cannot_convert`, `cannot_fetch`, `docx_password`, `docx_unreadable`, `hwp_distribution`, `hwp_password`, `no_text`, `nothing_found`, `page_count_unknown`, `pdf_empty`, `pdf_password`, `pdf_unreadable`, `scanned_pdf`, `unreadable`
  • 503 Error: `ai_off`, `auth_unavailable`, `busy`, `engine_unavailable`, `interrupted`, `storage_unavailable`, `unavailable`
post/v1/tasks/{tid}/processProcess (sync, or async with ?async=1 / webhook)

Sync (default, the current client): waits for the result, 200 {status:'done', download_url}. Async: with Prefer: respond-async or ?async=1 it answers 202 {status:'processing', status_url} at once; poll GET /v1/tasks/{id} until 'done' (then /download) or 'failed'. A sync call still running after GW_SYNC_WAIT s (default 90, under Cloudflare's 100 s proxy timeout; 0 = wait forever) also turns into that 202 instead of a Cloudflare 524 HTML page; the job keeps running. Kill-switch GW_ASYNC=0: always sync, no 202.

ParameterInTypeDescription
tid*pathstringtask_id
asyncquery1 | trueAnswer 202 at once
webhook_urlquerystring
Preferheaderrespond-async

Body: application/json (ProcessIn)

  • 200 Done (sync)
  • 202 Accepted; poll status_url. Also returned when a sync job runs over 90 s.
  • 400 Error: `bad_dpi`, `bad_fmt`, `bad_image`, `bad_lang`, `bad_level`, `bad_mode`, `bad_option`, `bad_page`, `bad_pages`, `bad_paper`, `bad_request`, `bad_sep`, `bad_size`, `bad_target`, `bad_typing`, `bad_url`, `bad_webhook_url`, `batch_empty`, `empty`, `empty_file`, `engine_error`, `no_url`, `not_pptx`, `not_xlsx`, `unknown_tool`, `webhooks_disabled`
  • 401 Error: `invalid_api_key`, `invalid_token`, `sign_in_required`, `token_expired`, `unauthorized`
  • 403 Error: `ai_not_in_free_api`, `blocked_url`, `custom_plan_required`, `disabled`, `forbidden`, `test_key_tool`
  • 404 Error: `unknown_tool`, `no_key`, `not_found`
  • 409 Error: `bad_state`, `not_ready`, `task_busy`, `too_many_keys`, `trial_key_exists`
  • 410 Error: `upload_expired`
  • 413 Error: `engine_error`, `batch_too_large`, `decompression_limit`, `docx_too_large`, `file_too_large`, `output_too_large`, `too_complex`, `too_large`, `too_many_files`, `too_many_pages`
  • 415 Error: `engine_error`, `legacy_doc`, `not_docx`, `not_epub`, `not_html`, `not_hwp`, `not_markdown`, `not_pdf`, `not_zip`, `unsupported_input`, `unsupported_media_type`
  • 422 Error: `bad_image`, `engine_error`, `not_docx`, `bad_pdf`, `cannot_compress`, `cannot_convert`, `cannot_fetch`, `docx_password`, `docx_unreadable`, `hwp_distribution`, `hwp_password`, `no_text`, `nothing_found`, `page_count_unknown`, `pdf_empty`, `pdf_password`, `pdf_unreadable`, `scanned_pdf`, `unreadable`
  • 429 Error: `api_credits_exceeded`, `engine_time_exceeded`, `free_translate_pages_exceeded`, `ip_quota_exceeded`, `ocr_pages_exceeded`, `output_quota_exceeded`, `quota_exceeded`, `rate_limited`, `too_many_concurrent`, `too_many_open_tasks`, `translate_pages_exceeded`
  • 500 Error: `engine_error`, `conversion_failed`, `internal_error`
  • 503 Error: `ai_off`, `auth_unavailable`, `busy`, `engine_unavailable`, `interrupted`, `storage_unavailable`, `unavailable`
get/v1/tasks/{tid}Task status (long-poll with ?wait=)

Task state. Long-poll: ?wait=<s> (capped at GW_LONGPOLL_MAX, default 20 s, under Cloudflare's 100 s) holds the answer while the task is processing and returns the moment it finishes, so a client sees 'done' with no poll-interval lag (the site polled every ~2 s: +1 s average per job) and makes 1 request instead of many. When done, signed_download_url (R2 presigned GET) is included so the client can skip GET /download (one round trip + CORS preflight less). Kill-switch GW_LONGPOLL=0: wait ignored, no signed link (old answer).

ParameterInTypeDescription
tid*pathstringtask_id
waitquerynumberHold up to N s (max 20) while processing
  • 200 Task
  • 401 Error: `invalid_api_key`, `invalid_token`, `sign_in_required`, `token_expired`, `unauthorized`
  • 404 Error: `unknown_tool`, `no_key`, `not_found`
delete/v1/tasks/{tid}Delete input and output now

Delete a task now: its input, parts and output objects in storage and its record. Only its owner (the same signed-in user, API key or guest cookie that created it); anyone else gets 404 (a task id is never confirmed). 204 on success, then GET / download answer 404. 409 task_busy while it is processing (retry when it finishes). 503 storage_unavailable if an object could not be deleted (the task is kept, so retrying is safe). Usage counts are not changed. Kill-switch GW_TASK_DELETE=0: 405 like before.

ParameterInTypeDescription
tid*pathstringtask_id
  • 204 Deleted
  • 401 Error: `invalid_api_key`, `invalid_token`, `sign_in_required`, `token_expired`, `unauthorized`
  • 404 Error: `unknown_tool`, `no_key`, `not_found`
  • 409 Error: `bad_state`, `not_ready`, `task_busy`, `too_many_keys`, `trial_key_exists`
  • 503 Error: `ai_off`, `auth_unavailable`, `busy`, `engine_unavailable`, `interrupted`, `storage_unavailable`, `unavailable`
get/v1/tasks/{tid}/downloadDownload link (or 302 with ?redirect=1)
ParameterInTypeDescription
tid*pathstringtask_id
redirectquery1 | true
  • 200 Signed link (15 min), or the bytes when storage has no presign
  • 302 Redirect to the signed link
  • 401 Error: `invalid_api_key`, `invalid_token`, `sign_in_required`, `token_expired`, `unauthorized`
  • 404 Error: `unknown_tool`, `no_key`, `not_found`
  • 409 Error: `bad_state`, `not_ready`, `task_busy`, `too_many_keys`, `trial_key_exists`

Tools

Tool list, per-tool upload field and caps.

get/v1/toolsTools
  • 200 Successful Response
get/v1/tools/{tool}Tool Info

One tool: how to upload for it (field file, repeated files, or none), the option it needs if it takes no file, its engine class (convert / ocr / ai: the global busy lanes) and the size / page / file caps per plan.

ParameterInTypeDescription
tool*pathstring
  • 200 Successful Response
  • 404 Error: `unknown_tool`, `no_key`, `not_found`

Usage

Plan, quota, credit and result-size meters.

get/v1/me/usagePlan, quota and meters
ParameterInTypeDescription
toolqueryToolId
refreshquery1
  • 200 Usage
  • 400 Error: `bad_dpi`, `bad_fmt`, `bad_image`, `bad_lang`, `bad_level`, `bad_mode`, `bad_option`, `bad_page`, `bad_pages`, `bad_paper`, `bad_request`, `bad_sep`, `bad_size`, `bad_target`, `bad_typing`, `bad_url`, `bad_webhook_url`, `batch_empty`, `empty`, `empty_file`, `engine_error`, `no_url`, `not_pptx`, `not_xlsx`, `unknown_tool`, `webhooks_disabled`
  • 401 Error: `invalid_api_key`, `invalid_token`, `sign_in_required`, `token_expired`, `unauthorized`
  • 503 Error: `ai_off`, `auth_unavailable`, `busy`, `engine_unavailable`, `interrupted`, `storage_unavailable`, `unavailable`
get/v1/me/jobsMe Jobs

Your API jobs (made with any of your keys) for the developer console: the last 100, and per day / per tool totals for the last days (1-90) days. credits is what each successful job spent from an API plan.

Sign-in access token only (not an API key).

ParameterInTypeDescription
daysqueryinteger
  • 200 Successful Response
  • 401 Error: `invalid_api_key`, `invalid_token`, `sign_in_required`, `token_expired`, `unauthorized`
  • 403 Error: `ai_not_in_free_api`, `blocked_url`, `custom_plan_required`, `disabled`, `forbidden`, `test_key_tool`
  • 404 Error: `unknown_tool`, `no_key`, `not_found`
  • 409 Error: `bad_state`, `not_ready`, `task_busy`, `too_many_keys`, `trial_key_exists`

Keys

API key management (sign-in token only; a key cannot manage keys).

get/v1/keysKeys List

Your API key (never the secret itself: last 4 characters), its usage this month and the webhook secrets (webhook_secret signs webhooks of jobs made with the key, account_webhook_secret those made while signed in).

Sign-in access token only (not an API key).

  • 200 Successful Response
  • 401 Error: `invalid_api_key`, `invalid_token`, `sign_in_required`, `token_expired`, `unauthorized`
  • 403 Error: `ai_not_in_free_api`, `blocked_url`, `custom_plan_required`, `disabled`, `forbidden`, `test_key_tool`
  • 404 Error: `unknown_tool`, `no_key`, `not_found`
  • 409 Error: `bad_state`, `not_ready`, `task_busy`, `too_many_keys`, `trial_key_exists`
post/v1/keysKeys

Self-serve API trial key (50 jobs/month). GW_TRIALKEY_AUTH (default on): only for a signed-in user (valid Supabase JWT, checked before anything else, so an anonymous caller learns nothing) and one key per user, ever: a second call is 409 trial_key_exists (a new key would be a fresh 50 jobs). GW_TRIALKEY_AUTH=0: the old anonymous endpoint. GW_ALLOW_TRIAL_KEYS=0 still turns the endpoint off.

Sign-in access token only (not an API key).

  • 200 Successful Response
  • 401 Error: `invalid_api_key`, `invalid_token`, `sign_in_required`, `token_expired`, `unauthorized`
  • 403 Error: `ai_not_in_free_api`, `blocked_url`, `custom_plan_required`, `disabled`, `forbidden`, `test_key_tool`
  • 404 Error: `unknown_tool`, `no_key`, `not_found`
  • 409 Error: `bad_state`, `not_ready`, `task_busy`, `too_many_keys`, `trial_key_exists`
delete/v1/keysKeys Revoke

Turn your API key off now (POST /v1/keys/rotate issues a new one on the same quota).

Sign-in access token only (not an API key).

  • 204 Successful Response
  • 401 Error: `invalid_api_key`, `invalid_token`, `sign_in_required`, `token_expired`, `unauthorized`
  • 403 Error: `ai_not_in_free_api`, `blocked_url`, `custom_plan_required`, `disabled`, `forbidden`, `test_key_tool`
  • 404 Error: `unknown_tool`, `no_key`, `not_found`
  • 409 Error: `bad_state`, `not_ready`, `task_busy`, `too_many_keys`, `trial_key_exists`
post/v1/keys/rotateKeys Rotate

New secret for your key: the old one stops working now, usage carries over (no fresh quota). For a lost or leaked key, or one turned off with DELETE /v1/keys. 404 no_key if you never made one (use POST /v1/keys).

Sign-in access token only (not an API key).

  • 200 Successful Response
  • 401 Error: `invalid_api_key`, `invalid_token`, `sign_in_required`, `token_expired`, `unauthorized`
  • 403 Error: `ai_not_in_free_api`, `blocked_url`, `custom_plan_required`, `disabled`, `forbidden`, `test_key_tool`
  • 404 Error: `unknown_tool`, `no_key`, `not_found`
  • 409 Error: `bad_state`, `not_ready`, `task_busy`, `too_many_keys`, `trial_key_exists`
post/v1/keys/extraKeys Extra Create

Another named key on your account, e.g. one per app or environment. mode "live" (default): same plan, quota and credits as your main key. mode "test": a sandbox key (sk_test_) with its own small daily allowance (api_test plan: 5 MB, 10 pages, no AI tools) that never touches your quota. Up to GW_MAX_EXTRA_KEYS (10). The secret is shown once. GW_EXTRA_KEYS=0 turns this off.

Sign-in access token only (not an API key).

  • 200 Successful Response
  • 401 Error: `invalid_api_key`, `invalid_token`, `sign_in_required`, `token_expired`, `unauthorized`
  • 403 Error: `ai_not_in_free_api`, `blocked_url`, `custom_plan_required`, `disabled`, `forbidden`, `test_key_tool`
  • 404 Error: `unknown_tool`, `no_key`, `not_found`
  • 409 Error: `bad_state`, `not_ready`, `task_busy`, `too_many_keys`, `trial_key_exists`
delete/v1/keys/extra/{kid}Keys Extra Delete

Delete one extra key now (id from GET /v1/keys). It stops working at once.

Sign-in access token only (not an API key).

ParameterInTypeDescription
kid*pathstring
  • 204 Successful Response
  • 401 Error: `invalid_api_key`, `invalid_token`, `sign_in_required`, `token_expired`, `unauthorized`
  • 403 Error: `ai_not_in_free_api`, `blocked_url`, `custom_plan_required`, `disabled`, `forbidden`, `test_key_tool`
  • 404 Error: `unknown_tool`, `no_key`, `not_found`
  • 409 Error: `bad_state`, `not_ready`, `task_busy`, `too_many_keys`, `trial_key_exists`

System

Health.

get/healthHealth

Cheap liveness by default. ?deep=1 (status page / health cron): also the engine's /health (cached 15 s); 503 when the engine is down so a monitor sees it. Kill-switch GW_DEEP_HEALTH=0: deep = cheap.

  • 200 Successful Response

Error codes with meaning, next step and retry advice: Errors & limits.