API docs
Hosted wkhtmltopdf for modern integrations.
PaperAPI delivers deterministic wkhtmltopdf renders with EU-only processing, retries, queueing, and observability. Use the same request for synchronous PDFs or background jobs—no separate async endpoint required.
Base URL: https://api.paperapi.de
Quickstart: stream a PDF
Send HTML + options and keep the default wait window (10s). The response streams application/pdf when the renderer finishes quickly. Add --output to save the file directly.
curl https://api.paperapi.de/v1/generate \
-H "Authorization: Bearer pk_live_xxx" \
-H "Accept: application/pdf" \
--json '{
"html": "<html><body><h1>Invoice</h1></body></html>"
}' \
--output invoice.pdfQuickstart: force async + poll
Add Prefer: respond-async to turn the same endpoint into a background job. Every 202 response includes createdAt, expiresAt, legacy downloadUrl, and links.self/result, plus Location and Retry-After headers so you know when to poll.
# 1) Enqueue with respond-async
curl https://api.paperapi.de/v1/generate \
-H "Authorization: Bearer pk_live_xxx" \
-H "Prefer: respond-async" \
--json '{
"html": "<html><body><p>Background job</p></body></html>"
}'
# 2) Poll the job (repeat until status == "Succeeded")
curl https://api.paperapi.de/jobs/{jobId} \
-H "Authorization: Bearer pk_live_xxx"
# 3) Download the PDF
curl https://api.paperapi.de/jobs/{jobId}/result \
-H "Authorization: Bearer pk_live_xxx" \
--output job.pdfAuthentication & headers
All endpoints require a workspace API key or authenticated session. Keys should only live on trusted servers. Idempotency-Key is highly recommended so retries do not create duplicate jobs; PaperAPI keeps the association for 24 hours and always returns the original job envelope when the payload matches.
Required
Authorization
Bearer pk_live_xxx
Issue or revoke keys in the dashboard. Keys are scoped per workspace.
Content-Type
application/json
Use curl --json or set the header manually to avoid HTTP 415 responses.
Recommended
Accept
application/pdf
Ensures pdf responses are streamed even if an HTTP client has a default JSON accept header.
Idempotency-Key
UUID / hash
Prevents duplicate jobs for matching payloads within 24h. Repeated requests return HTTP 202 with the original job envelope instead of rendering again.
Prefer
respond-async | wait=N
Add respond-async to return HTTP 202 immediately. Set wait=N (0–30, default 10) to cap how long the API waits for a synchronous PDF before falling back to a job.
Response headers
Location
Set by PaperAPI
All 202 responses include an absolute jobStatusUrl so you can poll without storing it yourself.
Retry-After
Set by PaperAPI
Returned on 202 (default 3 seconds) and 429 responses to tell clients when to retry.
X-Request-Id
Set by PaperAPI or forwarded from your proxy
Echoed on every response and added to error bodies as requestId for debugging.
Endpoints
Render HTML to PDF (sync or async). - Streams application/pdf when wkhtmltopdf completes inside the wait window (default 10 seconds).
- Use Prefer: respond-async to always enqueue a job, or Prefer: wait=N (0-30) to customize the synchronous wait cap.
- Idempotency-Key is recommended. Matching requests reuse the original job for 24 hours and respond with 202 + the existing job payload.
Headers
Authorization · Content-Type · Prefer (optional) · Idempotency-Key (optional)
Request
{
"html": "<html><body><h1>Invoice #512</h1></body></html>",
"options": {
"pageSize": "A4",
"marginTop": 12,
"printMediaType": true,
"disableSmartShrinking": true
}
}Responses
- 200 – application/pdf stream. Returned when rendering finishes before Prefer: wait=N expires.
- 202 – Job envelope with createdAt, expiresAt, legacy downloadUrl, and links.self/result. Includes Location + Retry-After headers.
- 400 – Missing html or invalid option (see error details).
- 401 – Missing/invalid API key or session.
- 413 – HTML payload larger than 500 KB.
- 429 – Monthly quota or rate limit exceeded. Respect Retry-After.
Example
curl https://api.paperapi.de/v1/generate \
-H "Authorization: Bearer pk_live_xxx" \
-H "Prefer: wait=5" \
--json '{
"html": "<html><body><p>Welcome</p></body></html>",
"options": { "pageSize": "A4" }
}' \
--output welcome.pdf- Returns the same envelope as the 202 response (id, status, createdAt, expiresAt, jobStatusUrl, downloadUrl, links).
- Call the Location header returned by POST /v1/generate or use the jobStatusUrl field stored in your system.
Responses
- 200 – Job envelope with optional errorMessage.
- 401 – Missing/invalid credentials.
- 404 – Job not found or does not belong to the authenticated workspace.
Example
curl https://api.paperapi.de/jobs/{jobId} \
-H "Authorization: Bearer pk_live_xxx"Download a completed PDF. - Streams the PDF for succeeded jobs and deletes the temporary file afterwards.
- Returns 404 until the job status becomes Succeeded.
Responses
- 200 – application/pdf stream.
- 401 – Missing/invalid credentials.
- 404 – Job still running, failed, or missing output.
Example
curl https://api.paperapi.de/jobs/{jobId}/result \
-H "Authorization: Bearer pk_live_xxx" \
--output job.pdfRetrieve the current month usage snapshot. - Returns used, monthlyLimit, remaining, overage, and nextRechargeAt (UTC month boundary).
- Fields clamp at zero so you can display progress bars without extra math.
Responses
- 200 – Usage summary JSON.
- 401 – Missing/invalid credentials.
Example
curl https://api.paperapi.de/v1/usage \
-H "Authorization: Bearer pk_live_xxx"
Return the active workspace profile. - Useful for debugging or showing the current plan inside internal tooling.
- Name falls back to the email prefix when no company info is set.
Responses
- 200 – Workspace id, name, email, plan metadata.
- 401 – Missing/invalid credentials.
Example
curl https://api.paperapi.de/v1/whoami \
-H "Authorization: Bearer pk_live_xxx"
Job model
Async responses and the /jobs/{jobId} endpoint return the following JSON. New fields (createdAt, expiresAt, and links) coexist with the legacy jobStatusUrl and downloadUrl fields for backward compatibility.
{
"id": "job-guid",
"status": "Queued | Processing | Succeeded | Failed",
"createdAt": "2025-04-02T12:00:00Z",
"expiresAt": "2025-04-09T12:00:00Z",
"errorMessage": "optional context",
"jobStatusUrl": "/jobs/{jobId}",
"downloadUrl": "/jobs/{jobId}/result",
"links": {
"self": "/jobs/{jobId}",
"result": "/jobs/{jobId}/result"
}
}- Queued– Job is waiting to be rendered (plan priority applies).
- Processing– wkhtmltopdf is actively generating the PDF.
- Succeeded– Download via downloadUrl or links.result.
- Failed– See errorMessage. Fix payload or retry if the failure was transient.
Options
All fields mirror wkhtmltopdf flags. Numbers can be sent as JSON numbers or strings. Start with the common options below, then layer advanced tweaks as needed.
Common
pageSize
Example: A4
Maps to --page-size
A4, Letter, Legal, etc. Default A4.
orientation
Example: portrait | landscape
Maps to --orientation
Case-insensitive. Default portrait.
margins
Example: marginTop/marginRight/marginBottom/marginLeft
Maps to --margin-top, --margin-right, --margin-bottom, --margin-left
Millimeters as decimals. Configure each side independently. Default 0mm.
printMediaType
Example: true
Maps to --print-media-type
Respect @media print rules. Default false.
disableSmartShrinking
Example: true
Maps to --disable-smart-shrinking
Avoid layout shifts from wkhtmltopdf auto-scaling. Default false.
header/footer text
Example: headerLeft/headerCenter/headerRight, footerRight
Maps to --header-left, --header-center, --header-right, --footer-left, --footer-center, --footer-right
Plain text placeholders (supports [page], [fromPage], etc.). Default empty.
headerHtml / footerHtml
Example: <html><body>Brand</body></html>
Maps to --header-html, --footer-html
Inline HTML snippets rendered from temp files so you never host separate URLs.
dpi & imageQuality
Example: dpi: 150, imageQuality: 90
Maps to --dpi, --image-quality
Higher values increase fidelity and file size. Defaults: dpi 96, imageQuality 94.
zoom
Example: 1.1
Maps to --zoom
Scale the output. >0, default 1.0.
images / noImages
Example: images: true | noImages: true
Maps to --images, --no-images
Force-enable or disable images explicitly. Leave both unset for wkhtmltopdf defaults.
enableJavascript / disableJavascript
Example: enableJavascript: true
Maps to --enable-javascript, --disable-javascript
JavaScript is disabled by default for deterministic renders. Toggle explicitly when needed.
Advanced
headerSpacing / footerSpacing
Example: headerSpacing: 5
Maps to --header-spacing, --footer-spacing
Spacing in millimeters between the header/footer and body. Default 0.
imageDpi
Example: 300
Maps to --image-dpi
Control raster image DPI independently from page DPI.
lowQuality
Example: true
Maps to --lowquality
Shortcut flag for smaller, faster renders. Default false.
header/footer placeholders
Example: [page] / [date]
Maps to --header-left, --footer-right
wkhtmltopdf supports [page], [fromPage], [toPage], [date], etc. Use text placeholders or HTML snippets.
Recommended defaults (invoice)
{
"options": {
"pageSize": "A4",
"orientation": "portrait",
"marginTop": 12,
"marginBottom": 12,
"printMediaType": true,
"disableSmartShrinking": true,
"footerRight": "Page [page] of [toPage]",
"dpi": 150,
"imageQuality": 90
}
}Client snippets
Handle both streaming and async responses by checking the status code. Below are minimal Node.js (fetch) and .NET HttpClient examples.
Node.js (fetch)
import fs from "node:fs/promises";
import fetch from "node-fetch";
const response = await fetch("https://api.paperapi.de/v1/generate", {
method: "POST",
headers: {
Authorization: "Bearer pk_live_xxx",
"Content-Type": "application/json",
},
body: JSON.stringify({ html: "<html><body>Hello</body></html>" }),
});
if (response.status === 202) {
const job = await response.json();
const status = await fetch(`https://api.paperapi.de${job.links.self}`, {
headers: { Authorization: "Bearer pk_live_xxx" },
});
console.log("Job status", await status.json());
} else if (response.ok) {
const pdf = Buffer.from(await response.arrayBuffer());
await fs.writeFile("invoice.pdf", pdf);
} else {
const error = await response.json();
console.error("PaperAPI error", error.requestId, error.details);
}.NET HttpClient
using System.Net;
using System.Net.Http.Json;
using System.Net.Http.Headers;
using var client = new HttpClient { BaseAddress = new Uri("https://api.paperapi.de") };
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "pk_live_xxx");
var payload = new { html = "<html><body>Hi</body></html>" };
using var response = await client.PostAsJsonAsync("/v1/generate", payload);
if (response.StatusCode == HttpStatusCode.Accepted)
{
var job = await response.Content.ReadFromJsonAsync<JobResponse>();
Console.WriteLine($"Job {job?.Id} queued at {job?.CreatedAt}");
}
else if (response.IsSuccessStatusCode)
{
var bytes = await response.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync("invoice.pdf", bytes);
}
else
{
var error = await response.Content.ReadFromJsonAsync<ErrorResponse>();
Console.WriteLine($"PaperAPI error {error?.RequestId}: {error?.Details}");
}
record JobLinks(string Self, string? Result);
record JobResponse(Guid Id, string Status, DateTimeOffset CreatedAt, JobLinks Links);
record ErrorResponse(string Error, string Details, string? RequestId);Errors
Every error response follows the envelope below. The optional requestId matches the X-Request-Id header, making it easy to forward logs to support.
{
"error": "invalid_payload",
"details": "The 'html' field is required.",
"requestId": "req_3b0c4f2f9a"
}- 400 – Validation failed (missing html or invalid options).
- 401 – Missing or invalid API key.
- 413 – HTML payload larger than 500 KB.
- 429 – Rate limit or plan quota exceeded. Retry with backoff or upgrade capacity.
- 500 – Renderer failed to produce a PDF (check errorMessage or requestId when contacting support).
Failure scenarios (curl)
# 401 - invalid key
curl -i https://api.paperapi.de/v1/generate --json '{ "html": "<p>Missing key</p>" }'
# 413 - payload too large
curl -i https://api.paperapi.de/v1/generate \
-H "Authorization: Bearer pk_live_xxx" \
--data-binary @huge.html
# 429 - respect Retry-After and Idempotency-Key
curl -i https://api.paperapi.de/v1/generate \
-H "Authorization: Bearer pk_live_xxx" \
-H "Prefer: respond-async" \
-H "Idempotency-Key: 2d6f7b7d-67af-4d6b-a5d2-123456789abc" \
--json '{ "html": "<p>Heavy load</p>" }'Rate limits & retries
Rate limits depend on your plan. Each response includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (Unix epoch seconds). Respect Retry-After on HTTP 202 and 429 responses, and apply exponential backoff before retrying idempotent requests.
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1714675200
Retry-After: 3
If you frequently hit the limit, reuse Idempotency-Key for automatic dedupe, shorten Prefer: wait=N, and reach out for dedicated capacity.