API reference
Docs
One endpoint does the work. Everything else — reading, listing, sharing, template samples — is free, because it costs no upstream work.
Quickstart
Sign in with your Ounie account, mint a key at /dashboard/api-keys, and post some markdown. A render takes about a second and costs 3 credits.
curl -X POST https://paperwork.ounie.com/api/documents \
-H "Authorization: Bearer ppw_live_…" \
-H "Content-Type: application/json" \
-d '{
"markdown": "# Q3 review\n\nThroughput rose **18%**.",
"template": "report",
"brand": { "name": "Northwind", "accent": "#b45309" },
"share": true
}'{
"ok": true,
"document": {
"id": "3f9a21c4-…",
"title": "Q3 review",
"template": "report",
"pages": 2,
"words": 412,
"bytes": 48210,
"warnings": [],
"shared": true,
"share_url": "https://paperwork.ounie.com/d/3f9a21c4-…",
"pdf_url": "https://…signed…"
},
"credits_spent": 3
}Render a document
/api/documents3 creditsSend markdown or blocks — one of the two is required. Everything else is optional.
| Field | Type | Meaning |
|---|---|---|
| markdown | string | GitHub-flavoured markdown. Headings, lists, tables, quotes, fenced code. Raw HTML is dropped and counted. A line containing only \pagebreak forces a new page. |
| blocks | Block[] | Structured blocks instead of markdown, when you want exact control. See the block schema below. |
| template | "report" | "onepager" | "memo" | "invoice" | "letter" | Default report. |
| title | string | If omitted and the body opens on an H1, that H1 becomes the title and is removed from the body. |
| subtitle | string | Standfirst under the title. |
| brand.name | string | Wordmark on the cover and running head. |
| brand.accent | string | Hex, e.g. #b45309. Darkened automatically if it would not reach 4.5:1 on paper. |
| brand.logoUrl | string | https PNG or JPEG, max 2MB. If it cannot be fetched the render still succeeds and says so in warnings. |
| pageSize | "a4" | "letter" | Default a4. |
| share | boolean | Also publish a hosted web page at /d/<id>. Free and revocable. |
warnings in the response is advisory, never fatal: it reports dropped HTML, an unreachable logo, or input clipped by a limit. A document that parses to nothing at all is a 422 and costs nothing.
Block schema
Every text field accepts inline markdown, so **bold**, *italic*, `code` and links work anywhere without hand-building a tree.
{ "type": "heading", "level": 1-4, "text": "…" }
{ "type": "paragraph", "text": "…" }
{ "type": "list", "ordered": false, "start": 1, "items": ["…"] }
{ "type": "quote", "text": "…" }
{ "type": "code", "language": "json", "text": "…" }
{ "type": "table", "columns": ["…"], "rows": [["…"]],
"align": ["left"|"center"|"right"] }
{ "type": "keyvalue", "items": [{ "label": "To", "value": "…" }] }
{ "type": "callout", "tone": "neutral"|"accent", "title": "…", "text": "…" }
{ "type": "signature", "name": "…", "role": "…" }
{ "type": "rule" }
{ "type": "pagebreak" }
{ "type": "spacer", "size": "sm"|"md"|"lg" }Two blocks are template-aware. A keyvalue block at the very top of a memo or invoice is hoisted into the letterhead — that is what makes the TO/FROM/DATE/RE grid and the invoice meta panel. Anywhere else it is rendered inline as a ruled definition list.
A letter with no signature block gets one automatically, under the brand name. It is the only place a template adds content, and only because the form requires it.
Templates
A cover page, running heads, folio numbers and a serif text face. For anything long enough to need finding your place again.
One dense sheet with a rule-topped masthead. Sans text, generous leading, built to be read standing up.
The classic TO / FROM / DATE / RE block over a ruled body. Short, dated, filed.
Brand block, bill-to, a real ruled table and a totals callout that stays with its table.
Letterhead, date, recipient block, salutation, and a signature rule at the end.
Sample PDFs render live from the same engine and are free — they use fixed content of ours, not yours, so nothing upstream is spent.
Reads & sharing
All of these are free, permanently. You already paid for the render.
/api/documents?limit=20free/api/documents/{id}free/api/documents/{id}free/api/documents/{id}/sharefree/api/documents/{id}/sharefreeEvery read returns a freshly signed pdf_url, valid for 24 hours. Signatures are never stored, so a link you copied yesterday expires while the document stays available.
A shared document is served at /d/<id> as a real web page, with /d/<id>/pdf minting a five-minute signature per click. Unpublishing therefore kills links that are already in the wild, which a long-lived signed URL could not.
MCP server
Streamable HTTP at https://paperwork.ounie.com/api/mcp. Works with Claude, Cursor, the AI SDK and the Ounie AI Team.
{
"mcpServers": {
"paperwork": {
"type": "http",
"url": "https://paperwork.ounie.com/api/mcp",
"headers": { "Authorization": "Bearer ppw_live_…" }
}
}
}Some hosts — the Ounie AI Team among them — cannot set a static header on a manual HTTP entry. The key rides the URL instead:
https://paperwork.ounie.com/api/mcp?api_key=ppw_live_…| Tool | Cost |
|---|---|
| render_document | 3 credits |
| get_document | free |
| list_documents | free |
| share_document | free |
| delete_document | free |
| list_templates | free · no auth |
| get_credit_balance | free |
| get_pricing | free · no auth |
| whoami | free |
Credit safety is server-side: a call that cannot be paid for is refused with insufficient_credits before any work happens, so an agent driving this in a loop can never overdraw. Credits live on ounie.com.
The app also accepts your ounie.com ounie_live_master key when you have enabled “Use across Ounie apps” in your ounie.com settings — one key for the whole fleet.
x402 — pay per call, no account
/api/x402/render$0.036 USDCSame body as /api/documents. USDC on Base at 36000 atomic units. Two rules govern the order of operations, and both exist because an on-chain settlement has no refund:
- 1.A bad request is refused free. An unsupported template, an empty body, or content that can never produce a page returns 4xx with no price quoted at all — you are never asked to sign an authorization for work that cannot be delivered.
- 2.Verify, render, then settle. The payment is verified before the work and settled only once a PDF exists. If the render fails, nothing moves on chain.
POST https://paperwork.ounie.com/api/x402/render
→ 402 {
"x402Version": 1,
"accepts": [{
"scheme": "exact",
"network": "base",
"maxAmountRequired": "36000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "0x…",
"extra": { "name": "USD Coin", "version": "2" }
}]
}
# sign, then retry
POST https://paperwork.ounie.com/api/x402/render
X-Payment: <base64 payload>
→ 200 { "document": { "pdf_url": "…" }, "payment": { "tx_hash": "0x…" } }An x402 document belongs to no account, so there is no dashboard from which anyone could delete it. Those files are kept for 30 days and then removed. Download the PDF; do not treat the link as permanent storage.
Errors
| Code | Status | Meaning |
|---|---|---|
| unauthorized | 401 | No credential. Send a session cookie, an Authorization: Bearer ppw_live_… , or ?api_key=. |
| invalid_payload | 400 | The body failed validation. `detail` names the offending fields. |
| invalid_request | 400 | x402 only: the same validation, refused before any price is quoted. |
| empty_document | 422 | Nothing renderable after parsing — usually an HTML paste. Nothing is charged. |
| insufficient_credits | 402 | Includes required_credits, balance_credits and buy_credits_url. Nothing is charged. |
| not_found | 404 | No such document for this owner. |
| storage_unavailable | 503 | Rendering is not configured on this deployment. Nothing is charged. |
| render_failed | 500 | The typesetter refused the document. Nothing is charged. |
| payment_invalid | 402 | x402 only: the X-Payment header did not verify. |
| payment_settlement_failed | 402 | x402 only: verified but the facilitator refused to settle. The document is withheld. |
Every non-2xx that can involve money says so explicitly. Credits are reserved before the work and restored to the exact pools they came from on any failure — there is no path in which a failed render leaves you short.
Limits
| Limit | Value |
|---|---|
| Markdown length | 400,000 characters |
| Blocks per document | 3,000 |
| List items | 500 |
| Table rows × columns | 800 × 12 |
| Code block length | 40,000 characters |
| Logo size | 2 MB, PNG or JPEG, https only |
| List endpoint | 100 documents per call |
| Active API keys | 5 per account |
Exceeding a limit clips the input and reports truncated in warnings rather than failing the render — a document with the last table row missing is more useful than a 400.
Something unclear or missing? Tell us.