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.pdfAuthentication
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_8Kj2m9qLxR4vNwT7bYcDgH3sFpZ1aE6uA 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: 840Pass 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.
| Field | Type | Description |
|---|---|---|
| html | string | Complete HTML document. Mutually exclusive with url. |
| url | string | Public URL to render. Mutually exclusive with html. |
| format | string | A4 (default), Letter, Legal, A3, A5, or Tabloid. |
| width / height | string | Custom page size, e.g. 210mm. Overrides format. |
| landscape | boolean | Rotate the page. Default false. |
| margin | string | object | Uniform value, or { top, right, bottom, left }. Default 10mm. |
| printBackground | boolean | Include CSS backgrounds and images. Default true. |
| scale | number | Render scale between 0.1 and 2. Default 1. |
| pageRanges | string | Subset to keep, e.g. 1-3,7. |
| header / footer | object | { height, html }. Supports pageNumber and totalPages spans. |
| waitFor | string | number | CSS selector or milliseconds to wait before capture. |
| media | string | Emulate screen or print stylesheets. Default print. |
| priority | boolean | Skip 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.
| Endpoint | What it does | Cost |
|---|---|---|
| POST /v1/merge | Combine several PDFs into one, in the order supplied. | $0.99 |
| POST /v1/split | Split by page ranges into separate documents. | $0.99 |
| POST /v1/rotate | Rotate pages by 90, 180 or 270 degrees. | $0.99 |
| POST /v1/compress | Downsample images and compress streams. | $0.99 |
| POST /v1/ocr | Add a searchable text layer to a scanned document. | $1.99 |
| POST /v1/watermark/remove | Strip the PDFeather free-plan watermark. | $1.49 |
| POST /v1/signatures | Send 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: 1787654400Free 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"
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Malformed JSON, unknown field, or html and url both supplied. |
| 401 | unauthorized | Missing, malformed, or revoked API key. |
| 403 | scope_denied | The key is valid but lacks the scope for this endpoint. |
| 402 | payment_required | Plan allowance exhausted and prepaid balance too low. |
| 404 | not_found | No such job, document or endpoint. |
| 409 | idempotency_conflict | Idempotency key reused with a different payload. |
| 413 | payload_too_large | Upload exceeds 50 MB, or HTML exceeds 5 MB. |
| 422 | render_failed | The page could not be rendered. See message for the cause. |
| 429 | rate_limited | Too many requests. Honour Retry-After. |
| 451 | content_blocked | The source URL is disallowed by our acceptable use policy. |
| 500 | internal_error | Our fault. Safe to retry with the same idempotency key. |
| 503 | engine_unavailable | Render capacity temporarily exhausted. Retry with backoff. |