API reference
PDFeather is software we host, so the interface to it is one REST API over HTTPS. JSON in, a PDF binary out. There is nothing to install: no engine on your side, no SDK, 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. Costs one extra document on top of the render. |
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. | 1 |
| POST /v1/split | Split by page ranges into separate documents. | 1 |
| POST /v1/rotate | Rotate pages by 90, 180 or 270 degrees. | 1 |
| POST /v1/compress | Downsample images and compress streams. | 1 |
| POST /v1/ocr | Add a searchable text layer to a scanned document. | 2 |
| POST /v1/watermark/remove | Remove the PDFeather watermark from a document we rendered. Our own mark only. | 2 |
| POST /v1/signatures | Send a document out for signature. | 3 |
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 documents.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
There is no subscription and nothing that renews. Documents come out of the free monthly allowance first, and after that each operation costs documents: a render costs 1 document, OCR costs 2, an e-signature request costs 3. Documents come from a package you buy once, and they do not expire. If you have too few documents for the next operation, the API returns 402 payment_required and stops. 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.
The first 25 documents each month are free and carry a PDFeather watermark. Anything paid with documents comes back clean.
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": "Free allowance used up and you have 0 documents. A render costs 1 document.",
"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 | Free allowance used up and too few documents for this operation. |
| 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. |