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:

  1. Get a live API key by completing onboarding (Get API access).
  2. Send an authenticated POST /api/v1/render request with your HTML.
  3. Save the returned application/pdf body to a file.
Save your key now. PaperSprocket reveals each raw API key only once; the original key cannot be recovered or shown again. If you still hold another valid account API key you can manage keys yourself (see Managing API keys). If you lose your only valid key, use email account recovery — never send the key or any other secret.
  • 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.

Core model: PaperSprocket does not require you to reshape your application data into a PaperSprocket-specific document schema. Your application builds the HTML; PaperSprocket turns that HTML into a PDF. If your application can produce HTML, it can use PaperSprocket.

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

FieldRequiredTypeDefaultAccepted values / rules
htmlyesstring—Non-empty string. The HTML document to render.
pagenoobject—Optional page configuration object.

Page object fields

FieldRequiredTypeDefaultAccepted values / rules
sizenostring"A4""A4" or "Letter".
orientationnostring"portrait""portrait" or "landscape".
margin_mmnoobject10 each sideObject with optional top, right, bottom, left. Each is a finite number between 0 and 50 (millimetres).
print_backgroundnobooleantrueWhether to print CSS backgrounds.
Unknown fields at any level are rejected with 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:

HeaderValue
Content-Typeapplication/pdf
Content-Dispositionattachment; filename="papersprocket-<render_id>.pdf"
Papersprocket-Render-IdUnique render identifier
Papersprocket-Page-CountAuthoritative number of pages in the PDF
Papersprocket-Charged-CentsAmount charged for this render (integer cents)
Papersprocket-Balance-CentsRemaining prepaid balance after this render (integer cents)

7. Errors

Errors return a JSON body shaped like {"error":"<code>","detail":"..."} (and render_id where applicable).

HTTPError codeMeaningRetry?
401unauthorizedMissing/malformed or invalid API keyafter fixing key
403live_key_requiredA test key was used against production; production requires a live-mode keyafter using live key
415unsupported_media_typeContent-Type is not application/jsonafter fixing header
415unsupported_content_encodingCompressed/encoded request bodies are not supportedafter sending identity body
400invalid_requestBody is not a valid JSON object / schema violationafter fixing body
413payload_too_largeRequest body exceeds the 2 MiB limitafter reducing size
402insufficient_fundsPrepaid balance cannot cover a renderafter adding credit
409render_in_progressThe same logical render is already executingwait, then retry
409idempotency_conflictIdempotency key reused with a different request bodyno — resolve key
422unsupported_contentDocument uses an unsupported asset/content type (see Limits)after removing asset
422invalid_pdfCompleted render was not a structurally valid PDFafter fixing document
422page_limit_exceededPDF exceeds the 50-page limitafter reducing pages
422output_too_largePDF exceeds the 20 MiB output limitafter reducing output
429account_render_busyAnother render is already running for this accountwait (~2s, Retry-After)
503renderer_busyGlobal render capacity temporarily exhaustedyes (Retry-After: 1)
503renderer_unavailableRenderer runtime temporarily unavailableyes after a short wait
504render_timeoutDocument exceeded the rendering time limityes, after simplifying
500internal_error / idempotent_replay_mismatch / accounting_errorUnexpected internal failure; no PDF was delivered and no charge was appliedcontact 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 requestIf 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 request409 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 retryRetry 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-Cents header 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 / behaviourValue
Request body limit2 MiB (larger → 413 payload_too_large)
Maximum page count50 pages per PDF (over → 422 page_limit_exceeded)
Output size limit20 MiB (over → 422 output_too_large)
Render time limitBounded per-render deadline (over → 504 render_timeout, no charge)
ConcurrencyAt most one render per account at a time; a small global capacity — further concurrent requests return 429/503 with a Retry-After hint.
JavaScriptDisabled. Documents must be static HTML/CSS, not scripts.
External / remote resourcesDisabled. Documents are network-dark. Any attempt to fetch a remote resource (http/https/file/relative, remote CSS, external SVG) fails the render.
Allowed inline assetsInline <svg>, and data: URLs for PNG, JPEG, WebP images and WOFF2 fonts.
Unsupported mediadata: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:

FieldMeaning
typepurchase (first $10 pack), topup (subsequent $10 pack), debit (a successful render), refund, or adjustment.
directioncredit (money in) or debit (usage consumed).
amount_centsPositive magnitude of the entry, in cents.
balance_after_centsBalance immediately after this entry, in cents.
statuscompleted or failed.
first_purchase1 for the account's first purchase/top-up; 0 otherwise.
reference_type / reference_idA stable external reference (e.g. the payment transaction for credits, or the render id for a debit).
description, created_atOptional 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>/apikeys authenticated with a valid account API key (no request body required; the key’s mode is set by the service, not chosen by you). Response contains account_id, key_id, mode, and the raw api_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>/apikeys with 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.

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.