API reference

One REST API over HTTPS. JSON in, a PDF binary out. There is no SDK to install and no client library to keep up to date. Every example below is a plain HTTP request.

Base URL https://api.pdfeather.com/v1 · all requests must be HTTPS · all responses are JSON unless the endpoint returns a document.

Quickstart

Create a key in the dashboard, export it as PDFEATHER_KEY, and send your first request. The response body is the PDF itself.

curl -X POST https://api.pdfeather.com/v1/render \
  -H "Authorization: Bearer $PDFEATHER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html": "<h1>Hello, PDF</h1>", "format": "A4"}' \
  --output hello.pdf

Authentication

Every request carries a bearer token in the Authorization header. Keys look like pf_live_… in production and pf_test_… in test mode.

Authorization: Bearer pf_live_8Kj2m9qLxR4vNwT7bYcDgH3sFpZ1aE6u

A key is shown in full exactly once, at creation. We store only its prefix and a SHA-256 hash, so a lost key cannot be recovered. Revoke it and mint a new one. Keys carry scopes (render, tools, ocr, sign); a request outside a key's scope is rejected with 403 scope_denied.

Treat keys as secrets: server-side only, never in client bundles or mobile apps. A request without a valid key returns 401 unauthorized.

Render a PDF

POST /v1/render accepts either html (a complete document string) or url (a publicly reachable page). Exactly one of the two is required. On success it responds 200 with Content-Type: application/pdf and the document as the body.

POST /v1/render HTTP/1.1
Host: api.pdfeather.com
Authorization: Bearer pf_live_••••••••
Content-Type: application/json
Idempotency-Key: 8f14e45f-ea6f-4b4f-9a1b-2c3d4e5f6a7b

{
  "url": "https://acme.com/invoices/42",
  "format": "A4",
  "margin": "20mm",
  "printBackground": true,
  "header": { "height": "15mm", "html": "<div class='t'>Acme Inc.</div>" },
  "footer": { "height": "12mm", "html": "<div class='p'>Page <span class='pageNumber'></span></div>" }
}

HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Length: 48213
X-Pdfeather-Job: job_2n8Kq4WmZ
X-Pdfeather-Cost-Cents: 99
X-Pdfeather-Duration-Ms: 840

Pass an Idempotency-Key header to make retries safe: repeating a request with the same key returns the original result instead of rendering (and billing) twice. Keys are remembered for 24 hours.

Render options

All options are optional except the html/url pair. Unknown fields are rejected with 400 invalid_request rather than ignored silently.

FieldTypeDescription
htmlstringComplete HTML document. Mutually exclusive with url.
urlstringPublic URL to render. Mutually exclusive with html.
formatstringA4 (default), Letter, Legal, A3, A5, or Tabloid.
width / heightstringCustom page size, e.g. 210mm. Overrides format.
landscapebooleanRotate the page. Default false.
marginstring | objectUniform value, or { top, right, bottom, left }. Default 10mm.
printBackgroundbooleanInclude CSS backgrounds and images. Default true.
scalenumberRender scale between 0.1 and 2. Default 1.
pageRangesstringSubset to keep, e.g. 1-3,7.
header / footerobject{ height, html }. Supports pageNumber and totalPages spans.
waitForstring | numberCSS selector or milliseconds to wait before capture.
mediastringEmulate screen or print stylesheets. Default print.
prioritybooleanSkip the queue. Adds $0.99 to the job.

Document tools

The tool endpoints accept multipart/form-data uploads (field name file, repeated for multi-file operations) or a JSON body with a url pointing at an existing document. Each returns the resulting document, or a job object when you pass "async": true.

EndpointWhat it doesCost
POST /v1/mergeCombine several PDFs into one, in the order supplied.$0.99
POST /v1/splitSplit by page ranges into separate documents.$0.99
POST /v1/rotateRotate pages by 90, 180 or 270 degrees.$0.99
POST /v1/compressDownsample images and compress streams.$0.99
POST /v1/ocrAdd a searchable text layer to a scanned document.$1.99
POST /v1/watermark/removeStrip the PDFeather free-plan watermark.$1.49
POST /v1/signaturesSend a document out for signature.$2.99

Async jobs

Long operations (batches, OCR over hundreds of pages) accept "async": true. You get a job immediately and either poll it or wait for the webhook.

POST /v1/ocr  { "url": "https://acme.com/scan.pdf", "async": true }

HTTP/1.1 202 Accepted
{ "id": "job_2n8Kq4WmZ", "status": "queued", "createdAt": "2026-08-26T09:12:04Z" }

GET /v1/jobs/job_2n8Kq4WmZ

HTTP/1.1 200 OK
{ "id": "job_2n8Kq4WmZ", "status": "succeeded", "costCents": 199,
  "durationMs": 14820, "resultUrl": "https://api.pdfeather.com/v1/jobs/job_2n8Kq4WmZ/result" }

Result links are single-use and expire after 24 hours. Statuses are queued, processing, succeeded, failed.

Webhooks

Available on Pro and above. Register an endpoint in the dashboard and we POST a signed JSON payload on job.succeeded, job.failed, signature.completed and balance.low.

POST https://your-app.com/hooks/pdfeather
PDFeather-Signature: t=1787654321,v1=5257a869e7...

{ "type": "job.succeeded", "id": "evt_9Qm2xK", "created": 1787654321,
  "data": { "id": "job_2n8Kq4WmZ", "costCents": 199 } }

Verify the signature by computing an HMAC-SHA256 of {timestamp}.{raw body} with your endpoint secret and comparing it to v1 in constant time. Reject timestamps older than five minutes. We retry failed deliveries with exponential backoff for 24 hours, so make your handler idempotent on id.

Billing behaviour

Subscription documents are drawn from your monthly allowance first. Once it is used up, each additional document costs $0.79 and is deducted from your prepaid balance. If the balance cannot cover it, the API returns 402 payment_required. We never charge a card mid-request.

Failed renders are not billed. Every successful response carries X-Pdfeather-Cost-Cents so you can reconcile against your own records, and the full ledger is available in the dashboard.

On the Free plan (25 documents a month) output carries a PDFeather watermark. Any paid plan removes it.

Rate limits

Limits are per API key and returned on every response. Exceeding them yields 429 rate_limited with a Retry-After header. Back off and retry rather than hammering.

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 1787654400

Free and Starter allow 20 requests per minute, Pro 120, Business 300, and Scale runs on a dedicated queue with limits agreed per account. The unauthenticated playground is limited per IP address.

Error codes

Errors are JSON with a stable code you can branch on, a human-readable message, and the requestId to quote in support requests.

HTTP/1.1 402 Payment Required
{
  "error": {
    "code": "payment_required",
    "message": "Plan allowance used up and balance is $0.12, which is below the $0.79 overage price.",
    "requestId": "req_7Yh3Nq2Lm"
  }
}
StatusCodeMeaning
400invalid_requestMalformed JSON, unknown field, or html and url both supplied.
401unauthorizedMissing, malformed, or revoked API key.
403scope_deniedThe key is valid but lacks the scope for this endpoint.
402payment_requiredPlan allowance exhausted and prepaid balance too low.
404not_foundNo such job, document or endpoint.
409idempotency_conflictIdempotency key reused with a different payload.
413payload_too_largeUpload exceeds 50 MB, or HTML exceeds 5 MB.
422render_failedThe page could not be rendered. See message for the cause.
429rate_limitedToo many requests. Honour Retry-After.
451content_blockedThe source URL is disallowed by our acceptable use policy.
500internal_errorOur fault. Safe to retry with the same idempotency key.
503engine_unavailableRender capacity temporarily exhausted. Retry with backoff.