PaperSprocket API Reference
Programmatic PDF generation for developers and automated systems. This reference describes the production API exactly as implemented. All examples use placeholder API keys — substitute your own live key.
1. Quickstart
Fastest path from API key to your first PDF:
- Get a live API key by completing onboarding (Get API access).
- Send an authenticated
POST /api/v1/renderrequest with your HTML. - Save the returned
application/pdfbody to a file.
- Production API URL:
https://papersprocket.com/api/v1/render - Authorization:
Authorization: Bearer YOUR_API_KEY - Content-Type:
application/json
curl -X POST https://papersprocket.com/api/v1/render \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: <your-unique-id>" \
-d '{"html":"<h1>Hello from PaperSprocket</h1>"}' \
-o hello.pdf
2. How integration works
PaperSprocket is an HTML-to-PDF rendering API. It turns a completed HTML document into a PDF. It is not a hosted template engine, and it does not define — or require you to use — a document-specific business-data schema.
Where PaperSprocket sits in your stack. You keep your existing data model and template logic. Your application converts its own business data into the complete HTML document you want printed, then sends that HTML — with optional page settings — to PaperSprocket, which renders the supplied HTML/CSS into a PDF and returns it.
your application data
→ your HTML / template logic
→ POST /api/v1/render { html, page }
→ PDF
HTML semantics. PaperSprocket understands HTML/CSS presentation semantics, not business semantics. It knows that <h1> is a heading, <p> is a paragraph, <table> is a table, and CSS controls layout. It does not need to know whether a value represents a customer name, an address, an invoice number, a VAT number, a total, a date, or any other domain field. To PaperSprocket, <p>John Smith</p> is simply content to render — it does not mean “customer name”.
What this means for you:
- No proprietary invoice, report, or document schema to learn.
- No field-by-field mapping of your data into PaperSprocket fields.
- No need to redesign an existing data model.
- Existing server-side template code can keep working unchanged.
- The integration boundary is simply HTML in → PDF out.
Minimal example
Your application holds ordinary domain data — for example an invoice object:
const invoice = {
customer: { name: "John Smith", address: "10 Main Street" },
invoiceNumber: "INV-1042",
total: 100.00
};
Your own code turns that data into HTML using your existing template logic:
const html = `
<html>
<body>
<h1>Invoice ${invoice.invoiceNumber}</h1>
<p>${invoice.customer.name}</p>
<p>${invoice.customer.address}</p>
<p>Total: €${invoice.total.toFixed(2)}</p>
</body>
</html>
`;
Then you send that HTML to PaperSprocket. The html field holds the finished document; the page object is optional:
await fetch("https://papersprocket.com/api/v1/render", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PSK_LIVE_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "invoice-1042"
},
body: JSON.stringify({ html })
});
Your business fields are not API fields. The PaperSprocket request schema covers only the rendering contract: html plus optional page settings (page.size, page.orientation, page.margin_mm, page.print_background). Customer fields such as customer_name, address, or amount never appear in the request — they live inside your own application and template.
v1 product boundary
PaperSprocket v1 is an HTML-to-PDF rendering API, not a hosted template or data-merging engine. This is a deliberate divide:
- You own: your source/business data, your field names, your template logic, and HTML construction.
- PaperSprocket owns: request validation, HTML/CSS rendering, PDF generation, page/output limits, and successful-page billing.
The selling point: no document-specific API schema. Keep your existing data model and templates; send the finished HTML.
3. Authentication
Provide your live API key in the Authorization header:
Authorization: Bearer YOUR_API_KEY
- Use the key server-side only. Never ship it in browser or client-side code.
- Never commit it to source control or logs.
- PaperSprocket reveals the raw key exactly once at claim time; only a hash is stored.
4. Render endpoint POST /api/v1/render
Generates a PDF from a self-contained HTML document. The body is a JSON object; unknown fields are rejected (closed schema).
Request body fields
| Field | Required | Type | Default | Accepted values / rules |
|---|---|---|---|---|
html | yes | string | — | Non-empty string. The HTML document to render. |
page | no | object | — | Optional page configuration object. |
Page object fields
| Field | Required | Type | Default | Accepted values / rules |
|---|---|---|---|---|
size | no | string | "A4" | "A4" or "Letter". |
orientation | no | string | "portrait" | "portrait" or "landscape". |
margin_mm | no | object | 10 each side | Object with optional top, right, bottom, left. Each is a finite number between 0 and 50 (millimetres). |
print_background | no | boolean | true | Whether to print CSS backgrounds. |
400 invalid_request. Omitting an optional value is the same as supplying its documented default.5. Working examples
cURL
curl -X POST https://papersprocket.com/api/v1/render \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-invoice-1042-001" \
-d '{"html":"<h1>Invoice</h1><p>Total: $100.00</p>","page":{"size":"A4","orientation":"portrait"}}' \
-o invoice.pdf
Note: on failure the API returns a JSON error body (never a PDF), e.g. {"error":"...","detail":"..."}. With -o an error response would be written into that file, so after a success confirm the saved file is a PDF (it starts with %PDF). To see errors directly, drop -o and add -i to print the status line and response body.
JavaScript / Node.js
import { writeFile } from "node:fs/promises";
const res = await fetch("https://papersprocket.com/api/v1/render", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PSK_LIVE_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "my-invoice-1042-001",
},
body: JSON.stringify({
html: "<h1>Invoice</h1><p>Total: $100.00</p>",
page: { size: "A4", orientation: "portrait" },
}),
});
if (!res.ok) {
// Print the API error body instead of saving an error response as a PDF.
console.error(await res.text());
process.exit(1);
}
await writeFile("invoice.pdf", Buffer.from(await res.arrayBuffer()));
Python
import os
import requests
res = requests.post(
"https://papersprocket.com/api/v1/render",
headers={
"Authorization": f"Bearer {os.environ['PSK_LIVE_KEY']}",
"Content-Type": "application/json",
"Idempotency-Key": "my-invoice-1042-001",
},
json={
"html": "<h1>Invoice</h1><p>Total: $100.00</p>",
"page": {"size": "A4", "orientation": "portrait"},
},
)
if not res.ok:
# Print the API error body instead of saving an error response as a PDF.
print(res.text)
raise SystemExit(1)
with open("invoice.pdf", "wb") as f:
f.write(res.content)
6. Success response
A successful render returns 200 with the PDF bytes as the body. Headers:
| Header | Value |
|---|---|
Content-Type | application/pdf |
Content-Disposition | attachment; filename="papersprocket-<render_id>.pdf" |
Papersprocket-Render-Id | Unique render identifier |
Papersprocket-Page-Count | Authoritative number of pages in the PDF |
Papersprocket-Charged-Cents | Amount charged for this render (integer cents) |
Papersprocket-Balance-Cents | Remaining prepaid balance after this render (integer cents) |
7. Errors
Errors return a JSON body shaped like {"error":"<code>","detail":"..."} (and render_id where applicable).
| HTTP | Error code | Meaning | Retry? |
|---|---|---|---|
| 401 | unauthorized | Missing/malformed or invalid API key | after fixing key |
| 403 | live_key_required | A test key was used against production; production requires a live-mode key | after using live key |
| 415 | unsupported_media_type | Content-Type is not application/json | after fixing header |
| 415 | unsupported_content_encoding | Compressed/encoded request bodies are not supported | after sending identity body |
| 400 | invalid_request | Body is not a valid JSON object / schema violation | after fixing body |
| 413 | payload_too_large | Request body exceeds the 2 MiB limit | after reducing size |
| 402 | insufficient_funds | Prepaid balance cannot cover a render | after adding credit |
| 409 | render_in_progress | The same logical render is already executing | wait, then retry |
| 409 | idempotency_conflict | Idempotency key reused with a different request body | no — resolve key |
| 422 | unsupported_content | Document uses an unsupported asset/content type (see Limits) | after removing asset |
| 422 | invalid_pdf | Completed render was not a structurally valid PDF | after fixing document |
| 422 | page_limit_exceeded | PDF exceeds the 50-page limit | after reducing pages |
| 422 | output_too_large | PDF exceeds the 20 MiB output limit | after reducing output |
| 429 | account_render_busy | Another render is already running for this account | wait (~2s, Retry-After) |
| 503 | renderer_busy | Global render capacity temporarily exhausted | yes (Retry-After: 1) |
| 503 | renderer_unavailable | Renderer runtime temporarily unavailable | yes after a short wait |
| 504 | render_timeout | Document exceeded the rendering time limit | yes, after simplifying |
| 500 | internal_error / idempotent_replay_mismatch / accounting_error | Unexpected internal failure; no PDF was delivered and no charge was applied | contact support |
For retryable failures with a Retry-After header, wait before retrying — but reusing the same Idempotency-Key with an identical body is safe and avoids double-charging.
8. Idempotency and retries
Idempotency-Key is required on every POST /api/v1/render request. It is an opaque, client-generated string matching ^[A-Za-z0-9._:-]{1,128}$ (letters, digits, and . _ : -; 1–128 characters). Use a fresh, unpredictable key for each distinct logical render, and reuse that same key for retries of a failed or ambiguous attempt.
| Same key, same request | If the original is still in progress you receive 409 render_in_progress — wait for the Retry-After hint, then retry with the same key. If it already succeeded, PaperSprocket re-renders it under the same render ID and verifies that the resulting page count and charge match the original successful render. The replay does not debit your balance again. If it failed in a retryable way the same render is re-opened and retried; a terminal failure returns the recorded failure. |
|---|---|
| Same key, different request | 409 idempotency_conflict. A key is bound to exactly one render request. Do not reuse a key for a different request body; each new render gets its own key. |
| Safe retry | Retry by re-sending the same Idempotency-Key with an identical JSON body. Because outcomes are keyed, a retry reuses or cleanly resumes the original render and never starts a duplicate or double-charges. |
Idempotency keys are only ever held as a one-way hash and are scoped to your account; the raw value is not stored or logged.
9. Billing
- PaperSprocket uses prepaid credits. A US$10 pack adds US$10 of prepaid balance (1,000 cents).
- Each successfully rendered page costs $0.02 (2 cents).
- You are charged only when a valid PDF is produced. Failed, partial, cancelled, or invalid renders are never charged.
- The page count (and charge) is derived from the authoritative completed PDF — the renderer's approximation is not used for billing.
- Remaining balance is returned in the
Papersprocket-Balance-Centsheader on every successful render, and via the balance endpoint below. - Because of
Idempotency-Key, a replayed succeeded render is re-rendered under the same render ID and never debits twice.
10. Limits and rendering behaviour
| Limit / behaviour | Value |
|---|---|
| Request body limit | 2 MiB (larger → 413 payload_too_large) |
| Maximum page count | 50 pages per PDF (over → 422 page_limit_exceeded) |
| Output size limit | 20 MiB (over → 422 output_too_large) |
| Render time limit | Bounded per-render deadline (over → 504 render_timeout, no charge) |
| Concurrency | At most one render per account at a time; a small global capacity — further concurrent requests return 429/503 with a Retry-After hint. |
| JavaScript | Disabled. Documents must be static HTML/CSS, not scripts. |
| External / remote resources | Disabled. Documents are network-dark. Any attempt to fetch a remote resource (http/https/file/relative, remote CSS, external SVG) fails the render. |
| Allowed inline assets | Inline <svg>, and data: URLs for PNG, JPEG, WebP images and WOFF2 fonts. |
| Unsupported media | data:image/svg+xml and GIF images are not supported and will fail the render. |
11. Balance and usage
An authenticated balance endpoint exists for checking prepaid balance:
GET https://papersprocket.com/api/v1/accounts/<ACCOUNT_ID>/balance
Authorization: Bearer YOUR_API_KEY
Response:
{"account_id":"...","balance_cents":1000,"currency":"USD","page_price_cents":2}
In addition to the balance endpoint there is an append-only ledger of credit and debit history: GET /api/v1/accounts/<ACCOUNT_ID>/ledger (account API key required). Each entry has the following fields:
| Field | Meaning |
|---|---|
type | purchase (first $10 pack), topup (subsequent $10 pack), debit (a successful render), refund, or adjustment. |
direction | credit (money in) or debit (usage consumed). |
amount_cents | Positive magnitude of the entry, in cents. |
balance_after_cents | Balance immediately after this entry, in cents. |
status | completed or failed. |
first_purchase | 1 for the account's first purchase/top-up; 0 otherwise. |
reference_type / reference_id | A stable external reference (e.g. the payment transaction for credits, or the render id for a debit). |
description, created_at | Optional human-readable note and the entry timestamp. |
Finding your account ID — the balance, ledger, top-up, and key-management endpoints all take <ACCOUNT_ID> in the URL. When you complete onboarding your account ID is shown next to your API key on the key-ready screen (and is also returned as account_id by the onboarding API). Keep it with your key. It is an account identifier, not a secret, and it is fine — and useful — to include it in support requests. Never send it together with your API key, credentials, document contents, or any other secret.
12. Topping up your balance
PaperSprocket is prepaid and optional top-ups are sold the same way as your first pack: US$10 of prepaid credit (500 pages at $0.02/page), no subscription. There is no auto-debit — when prepaid balance reaches zero, renders fail with 402 insufficient_funds (plus a Retry-After hint) until you top up.
To buy a top-up, start an authenticated checkout with your account API key:
POST https://papersprocket.com/api/v1/accounts/<ACCOUNT_ID>/checkout
Content-Type: application/json
Authorization: Bearer YOUR_OWNER_API_KEY
Response (abbreviated):
{
"account_id": "...",
"checkout": {
"url": "https://checkout.paddle.com/..."
}
}
Open the returned checkout.url, complete the US$10 payment in Paddle, and you are done. Payments and refunds are processed by Paddle as Merchant of Record — PaperSprocket never sees your card details. Your balance is credited after Paddle confirms the payment is complete — not merely after you are redirected back. Confirm arrival by re-reading GET /api/v1/accounts/<ACCOUNT_ID>/balance — after the pack lands, balance_cents increases by 1000.
Each checkout you start is a separate purchase: complete one checkout before starting another, and treat each returned URL as its own $10 pack. The checkout URL itself is not proof of payment and carries no authority to credit your account — only the verified webhook grants balance.
13. Managing API keys
Onboarding issues exactly one initial account API key for your account. It is shown only once (on the key-ready screen) and cannot be recovered — the original raw key is never stored and can never be shown again. If your lost or compromised key was your only valid account API key, the key-management endpoints require a valid account API key to authenticate, so you cannot self-serve a reset through the API. In that case use email account recovery to prove control of your registered address and issue a new key. Account API keys authenticate operations on your account and are scoped to that account.
- Issue another key:
POST /api/v1/accounts/<ACCOUNT_ID>/apikeysauthenticated with a valid account API key (no request body required; the key’s mode is set by the service, not chosen by you). Response containsaccount_id,key_id,mode, and the rawapi_key— shown once. Use this to issue an additional or replacement key while you still hold a valid account API key to authenticate this request. - List your keys (metadata only):
GET /api/v1/accounts/<ACCOUNT_ID>/apikeyswith a valid account API key. Returns key ids, mode, created/revoked times and prefixes — never raw secrets. - Revoke a key:
DELETE /api/v1/accounts/<ACCOUNT_ID>/apikeys/<KEY_ID>with a valid account API key. Once revoked the key immediately stops authenticating.
Lost or compromised key: if you still hold another valid account API key, revoke the compromised key and issue a replacement (above). If you no longer have a usable key, use email account recovery instead: PaperSprocket verifies control of your registered email and issues one new key. Recovery revokes all existing active keys for the account (because if a key is genuinely lost, PaperSprocket cannot know whether it was lost or compromised), so before you run it, be certain you do not need any current key. Old raw API keys can never be recovered or re-displayed — never send the lost or compromised key or any other secret, and support will never ask you for one. Do not keep multiple copies of a live key in more places than necessary, and never commit a key to source control, logs, or public documents.
14. Legal and support
- Terms of Service
- Privacy Policy
- Refund Policy
- Support: support@papersprocket.com
Getting help. When you contact support, include your account_id, the HTTP status and error code you received, and (for render failures) the render id from the Papersprocket-Render-Id header. Never send an API key, a full document, or any other secret or sensitive content in a support email — this is a support channel for account and billing help, not a place to transmit confidential documents or credentials. Refunds and billing issues follow the Refund Policy.