Leafwright API
API documentation
API keys belong to an organization project. Use test keys for development and live keys for production traffic.
Authentication
Bearer API keys
Example
Authorization: Bearer <your API key>Example
# Dashboard → API keys → "Create test key". The key is shown once.
# Every example on this page reads it from one shell variable:
export LEAFWRIGHT_API_KEY="paste-your-key-here"Render
HTML to PDF
Engine: Chromium 151 via Playwright 1.62, version-pinned. Upgrades are deliberate — the pin changes in a reviewed release, never on a rebuild — and this page states the running version.
Example
# 1. Render HTML and save the PDF bytes to a file
curl -X POST https://leafwright.co/api/v1/pdfs \
-H "Authorization: Bearer $LEAFWRIGHT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"source":{"type":"html","html":"<h1>Hello from Leafwright</h1>"},"delivery":{"mode":"sync","return":"bytes"}}' \
-o hello.pdfExample
# 2. Or store the PDF and get a signed download URL back (expires in 15 minutes)
curl -X POST https://leafwright.co/api/v1/pdfs \
-H "Authorization: Bearer $LEAFWRIGHT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": { "type": "html", "html": "<h1>Hello from Leafwright</h1>" },
"options": { "format": "A4", "print_background": true },
"delivery": { "mode": "sync", "return": "url" },
"filename": "hello.pdf"
}'Example
{
"id": "doc_...",
"job_id": "job_...",
"status": "succeeded",
"download_url": "https://leafwright.co/api/v1/documents/doc_.../download?token=...",
"expires_at": "2026-09-02T12:15:00.000Z",
"metadata": { "pages": 1, "file_size_bytes": 18321, "render_duration_ms": 1400, "credits_used": 1 }
}Example
# 3. Render a public web page instead of HTML
curl -X POST https://leafwright.co/api/v1/pdfs \
-H "Authorization: Bearer $LEAFWRIGHT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"source":{"type":"url","url":"https://leafwright.co/pricing"},"delivery":{"mode":"sync","return":"bytes"}}' \
-o pricing.pdfExample
# 4. Read the render logs for any job (key needs the logs:read permission)
curl https://leafwright.co/api/v1/jobs/$JOB_ID/logs \
-H "Authorization: Bearer $LEAFWRIGHT_API_KEY"Example
# 5. Read what was sent to the renderer for a job (also logs:read)
curl https://leafwright.co/api/v1/jobs/$JOB_ID/input \
-H "Authorization: Bearer $LEAFWRIGHT_API_KEY"
{
"data": {
"jobId": "job_...",
"sourceType": "html",
"payload": {
"source": { "type": "html", "html": "<h1>Hello from Leafwright</h1>" },
"options": { "format": "A4", "print_background": true },
"filename": "hello.pdf",
"delivery": { "mode": "sync", "return": "url", "webhookOrigin": null }
},
"sizeBytes": 18321,
"truncated": false,
"retentionExpiresAt": "2026-09-11T12:00:00.000Z"
}
}
The captured input expires with the document it produced, on the project's
retention window, and is then deleted. After that the endpoint answers
{ "data": null, "reason": "expired" }; a job rendered before capture existed
answers { "data": null, "reason": "not_captured" }.
For a template render the payload holds the compiled HTML the renderer
received, with your data already merged in. Only the origin of a delivery
webhook is kept, never its path or query, which routinely carry a secret.
Sources larger than 256 KB are stored truncated, with "truncated": true.Example
Use delivery.return to choose the response route:
"url" store the PDF and return a signed download_url (15 minute TTL)
"metadata" store the PDF and return job/document metadata
"bytes" return application/pdf bytes directly; requires mode "sync"
Errors come back as JSON: { "error": { "code", "message", "details" } }.
401 MISSING_SESSION or INVALID_SESSION for a missing or bad key,
402 USAGE_LIMIT_REACHED, 429 RATE_LIMITED (30 renders per minute per key),
400 BAD_REQUEST when the HTML fails validation, 400 SECURITY_BLOCKED_URL
when a URL source points at a private network.Example
HTML rules — validated before rendering (applies to raw HTML and templates):
Supported full documents (doctype, html, head, meta, title, body, style),
text, heading, list, table, and layout tags, and <img> with
base64 data URIs (png, jpeg, gif, webp, svg+xml), relative
paths, or cid: references. aria-* and data-* attributes pass.
Rejected <script>, <iframe>, <object>, <embed>, <form>, <link>, inline
event handlers, external http(s) URLs in src or CSS url(),
and CSS @import. The renderer never fetches remote assets for
HTML sources — embed images as data URIs. To capture a public
web page with its assets, use source type "url" instead.
SVG Inline <svg> markup is not supported (nor SVG in CSS url()).
Base64-encode the SVG and embed it via <img>:
<img src="data:image/svg+xml;base64,..."> — the data URI must
be base64; URL-encoded SVG data URIs are rejected.
Limits HTML 250 KB, CSS 120 KB, 200 pages per PDF.
Validation errors are returned in the 400 response body and name the
offending tag, attribute, or URL.Templates
Create, preview, publish, render
Example
POST /api/v1/templates
PATCH /api/v1/templates/{template_id}
POST /api/v1/templates/{template_id}/preview
POST /api/v1/templates/{template_id}/publish
POST /api/v1/templates/{template_id}/render
POST /api/v1/templates/{template_id}/duplicate
POST /api/v1/templates/{template_id}/archive
POST /api/v1/templates/{template_id}/versions/{version_id}/restore
POST /api/v1/templates/ai/generate
POST /api/v1/templates/{template_id}/validate
Template renders automatically receive project brand data under:
brand.logo_url
brand.primary_color
brand.accent_color
brand.footer_text
brand.disclaimer
brand.support_email
brand.website_url
AI template endpoints enforce prompt safety, per-request token estimates,
daily request limits, and monthly AI token limits by billing plan before
calling AI Gateway.
Archived templates cannot be previewed, published, or rendered. Restore copies
a published version back into the draft, where it can be reviewed and published
as a new immutable version.Brand
Project document defaults
Example
# Dashboard-session endpoint: the Leafwright dashboard calls this with a
# signed-in user session, not a project API key. Set brand defaults in
# Dashboard → Brand; template renders pick them up automatically.
PATCH /api/v1/dashboard/brand
Authorization: Bearer <supabase_access_token>
{
"project_id": "proj_...",
"logo_url": "https://cdn.example.com/logo.png",
"primary_color": "#14211b",
"accent_color": "#c87845",
"footer_text": "Generated by Acme",
"disclaimer": "Confidential document.",
"support_email": "support@example.com",
"website_url": "https://example.com"
}
POST /api/v1/dashboard/brand/upload-url
POST /api/v1/dashboard/brand/references
GET /api/v1/dashboard/brand/references?project_id=proj_...Webhooks
Render events
Example
# Dashboard-session endpoint: create subscriptions in Dashboard → Webhooks.
POST /api/v1/dashboard/webhooks
Authorization: Bearer <supabase_access_token>
{
"project_id": "proj_...",
"url": "https://example.com/leafwright/webhook",
"events": ["render.succeeded", "render.failed"]
}
Webhook deliveries include x-leafwright-event, x-leafwright-timestamp,
and x-leafwright-signature headers.
POST /api/v1/dashboard/webhooks/deliveries/{delivery_id}/retry
Authorization: Bearer <supabase_access_token>
GET or POST /api/v1/cron/webhooks/retry
Authorization: Bearer <cron_secret>Key rotation
Rotate without sharing secrets twice
Example
POST /api/v1/api-keys/{key_id}/rotate
Authorization: Bearer <supabase_access_token>
Response includes a one-time replacement key and revokes the previous key.Billing
Plans and overages
Example
POST /api/v1/billing/checkout
Authorization: Bearer <supabase_access_token>
{
"org_id": "org_...",
"plan_slug": "starter"
}
Paid plans can include metered overages. Free plans remain hard-capped.