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.pdf
Example
# 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.pdf
Example
# 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.