API Reference

Base URL: https://api.pdfkraft.com/api/v1

Authentication

All API requests must include your API key in the Authorization header. Your key is shown once at registration and can be regenerated from the Account page.

Authorization: Bearer YOUR_API_KEY

Besides the account's primary key, you can create scoped keys with limited permissions — recommended for integrations.

API keys & scopes

Scoped keys are created on the API Keys page (or via the endpoints below using a dashboard session) and can be limited two ways:

RestrictionEffect
scopes: ["generate"]Can generate documents and read templates, but any template write (create, update, publish, delete) returns 403 insufficient_scope. ["full"] keys behave like the primary key.
template_ids / folder_idsNon-empty lists restrict generation to those templates (or templates in those folders). Restricted keys also cannot use raw_html. Empty lists (default) allow all templates.

Key management

Method & pathDescription
GET /api_keys/currentIntrospect the calling key: its scopes and restrictions, plus your plan and quota. Works with any API key — use it to discover what a pasted key may do.
GET /api_keysList scoped keys. Dashboard session only.
POST /api_keysCreate a scoped key ({name, scopes, template_ids, folder_ids}). Returns the full key once. Dashboard session only — a leaked API key cannot mint further keys.
DELETE /api_keys/:idRevoke a key. Takes effect immediately. Dashboard session only.

GET /api_keys/current — response 200

{
  "api_key": {
    "id": "key-uuid",          // null for the account's primary key
    "name": "n8n production",  // null for the primary key
    "prefix": "89c32ffb",
    "scopes": ["generate"],
    "template_ids": [],
    "folder_ids": []
  },
  "plan": "starter",
  "effective_plan": "starter",
  "on_trial": false,
  "quota": {
    "limit": 500,
    "used": 123,
    "remaining": 377,
    "resets_at": "2024-02-01T00:00:00.000Z"
  }
}

Rate limits

LimitValue
Global (all authenticated routes)60 requests / minute per API key
Document generation (POST /documents, /documents/sync, /documents/preview) — Free10 requests / minute per API key
Document generation — Starter30 requests / minute per API key
Document generation — Pro120 requests / minute per API key
Login10 requests / minute per IP
Register · Forgot password5 requests / minute per IP
Reset password10 requests / minute per IP

When rate-limited the API returns 429 with code: "rate_limited" and a Retry-After header indicating seconds to wait. Authenticated limits are keyed by API-key; auth-endpoint limits are keyed by client IP.

Every document-generation response also carries the current window state, so integrations can throttle before hitting a 429:

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1783353144   // Unix epoch seconds

Doing a large batch run (e.g. a month-end invoice run)? Pace requests to stay under your tier rather than bursting — there is currently no separate batch endpoint.

Idempotency

Generation requests (POST /documents, POST /documents/sync) accept an optional Idempotency-Key header. Retrying with the same key within 24 hours returns the original document instead of generating (and billing) again — safe for network timeouts and at-least-once job queues.

curl -X POST https://api.pdfkraft.com/api/v1/documents/sync \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: order-8412-invoice" \
  -H "Content-Type: application/json" \
  -d '{ "document_template_id": "...", "payload": { ... } }'
  • Replayed responses include the header Idempotency-Replayed: true.
  • If the first request with a key is still in flight, a concurrent retry gets 409 idempotency_key_in_flight — retry after a moment.
  • Keys are scoped to your account and expire after 24 hours.

Documents

POST/documents— async generation

Enqueue a document for generation. Returns immediately with a document id and status: "pending". Poll GET /document_cards/:id or use webhooks to know when it's ready.

Request body

{
  // One of these is required:
  "document_template_id": "uuid",
  "raw_html": "<html>...</html>",

  // Optional:
  "payload": { "key": "value" },
  "meta": { "_webhook_channel": "orders" },
  "filename": "invoice_{{number}}.pdf",
  "password": "secret",
  "output_type": "pdf" | "png" | "jpg" | "webp",
  "reproducible": false,
  "strict_variables": false,
  "ttl": 3600,
  "share": true,
  "template_version": 3
}

Response 201

{
  "id": "doc-uuid",
  "status": "pending",
  "created_at": "2024-01-01T00:00:00.000Z",
  "unresolved_variables": []
}

Fields

FieldTypeDescription
document_template_idstring (UUID)ID of a published template. Mutually exclusive with raw_html.
raw_htmlstringRaw HTML to render directly. Mutually exclusive with document_template_id. When a non-empty payload is supplied, the HTML is rendered as a Liquid template with those variables (snippets included); without a payload it renders as-is and {{ }} passes through literally. External assets (images, fonts, stylesheets) must be reachable at public URLs — requests to loopback, link-local, or private (RFC 1918) addresses are blocked by the renderer. See Images & assets.
payloadobjectVariables merged into the Liquid template. Default: {}.
metaobjectArbitrary metadata stored with the document. Use _webhook_channel to route webhook events.
filenamestringOutput filename. Supports {{variable}} placeholders replaced from payload. Sanitized to a single safe name — path separators and control characters are stripped. Default: auto-generated.
passwordstringPassword-protect the PDF. Only applies to PDF output. If omitted, the template's default password (if set) applies. Write-only — it is never returned in document cards, webhook payloads, or the meta object.
output_typeenumpdf, png, jpg, or webp. Overridden by the template's output type when using a template.
reproduciblebooleanPDF output only. Normalizes generation metadata (timestamps, document ID, title) so identical requests produce byte-identical files — useful for caching, diffing, and snapshot tests. Guaranteed on unchanged infrastructure only (renderer upgrades may alter output) and not compatible with password. Default false.
strict_variablesbooleanWhen true, reject the request with 422 unresolved_variables instead of generating if the payload leaves any template variable unresolved. See Liquid templating. Default false — generation proceeds either way, and the resolved/unresolved list is always reported (see below).
ttlinteger (seconds)Per-request retention override. Shortens (never extends) the plan's retention window and any template-level ttl — use it to make a specific document expire quickly for PII hygiene.
sharebooleanPro plan only. Set to false to omit public_share_link entirely — no unauthenticated share URL is created for this document. Default true.
template_versionintegerPin generation to a specific published snapshot from GET /document_templates/:id/versions, so a publish that happens after you enqueue this request can't change what it renders. 404 template_version_not_found if the version doesn't exist. See Templates.

Every generation response — async, sync, and preview — reports unresolved_variables: the dotted payload paths the template reads that this payload leaves genuinely blank (a missing key, or a missing array the template loops over). An explicit null or "" doesn't count — that's a value you supplied on purpose, usually for a field the template already guards with {% if %}. On the async route it's in the immediate response body; on sync/preview it's in the returned card's meta.unresolved_variables.

POST/documents/sync— synchronous generation

Same request body as POST /documents. Blocks until generation completes and returns the full document card. Timeout is 30 s on Free/Starter and 120 s on Pro. Returns 201 on success or 500 on generation failure — the failure response is still a document card, with status: "failure", a human-readable failure_cause, and a machine-readable failure_code (mirrored as code).

Add "dry_run": true to render exactly like /documents/preview instead — no quota reservation, no webhooks, no public share link, and the file expires after 1 hour. Useful for a one-line "test this request without committing to it" toggle without changing endpoints. Ignores Idempotency-Key — a dry run is never a real document, so there is nothing to dedupe.

Example request

curl -X POST https://api.pdfkraft.com/api/v1/documents/sync \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "document_template_id": "your-template-id",
    "payload": { "name": "Alice", "total": 99.00 },
    "filename": "invoice_{{name}}.pdf"
  }'

Response 201

{
  "id": "doc-uuid",
  "status": "success",
  "filename": "invoice_Alice.pdf",
  "download_url": "https://storage.pdfkraft.com/...",
  "public_share_link": null,
  "output_type": "pdf",
  "document_template_identifier": "my-invoice",
  "failure_cause": null,
  "failure_code": null,
  "meta": {},
  "created_at": "2024-01-01T00:00:00.000Z",
  "generated_at": "2024-01-01T00:00:01.234Z"
}
POST/documents/preview— dry-run render (does not count quota)

Renders through the exact production pipeline but never reserves quota, fires webhooks, or creates share links; the file expires after 1 hour. Use it to test payloads during integration development. Same body as POST /documents, plus use_draft: true to render a template's current draft instead of its published version, and strict_variables to get a 422 instead of a blank field for anything the payload leaves unresolved. Counts toward your plan's generation rate limit — see Rate limits.

curl -X POST https://api.pdfkraft.com/api/v1/documents/preview \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "document_template_id": "your-template-id",
    "use_draft": true,
    "payload": { "name": "Alice" }
  }'
GET/document_cards— list documents

Returns a paginated list of document cards. 24 per page.

Query paramTypeDescription
pageintegerPage number, default 1.
statusstringFilter: pending, generating, success, failure.
template_idstring (UUID)Filter by template.
{
  "data": [ /* document card objects */ ],
  "total": 42,
  "page": 1,
  "pages": 2
}
GET/document_cards/:id— get a document

Returns a single document card with a fresh presigned download_url (1-hour validity). Poll this endpoint after async generation to check for completion.

Polling pattern: After POST /documents, poll GET /document_cards/:id every second until status is success or failure. Alternatively, use POST /documents/sync to block until done.
GET/documents/:id/download— download file

Generates a fresh presigned URL and redirects (302) to the file in object storage. Returns 400 if the document has not yet been generated.

Document card object

FieldTypeDescription
idstring (UUID)Document ID.
statusenumpendinggeneratingsuccess or failure.
filenamestring | nullFinal filename. Null until generation completes.
download_urlstring | nullPresigned download URL, valid for 1 hour. Null until status is success.
public_share_linkstring | nullPermanent public URL (Pro plan only). Null on Free/Starter.
output_typeenumpdf, png, jpg, or webp.
document_template_identifierstring | nullIdentifier slug of the template used, or null for raw HTML.
failure_causestring | nullA user-safe, human-readable reason generation failed — a category such as a rendering, template, stylesheet, or storage error. Non-null only when status is failure.
failure_codestring | nullStable machine-readable code for the failure — branch on this instead of string-matching failure_cause. See Errors for the full list.
metaobjectMetadata stored at creation time, plus unresolved_variables (see above). password is never included even if you set one.
created_atISO 8601When the document was submitted.
generated_atISO 8601 | nullWhen generation completed. Null until status is success.

Templates

Templates are HTML + SCSS documents with Liquid syntax for dynamic data. Create and edit them in the dashboard or entirely via the API. Every template has a draft and a published version — writes always target the draft; publishing copies the draft live. A template must be published before it can be used for generation. Template writes require a full-scope key.

POST/document_templates— create (and optionally publish)

Creates a template. body, scss_style, sample_data, and settings write the draft (the dashboard-style *_draft names are also accepted). Pass publish: true to publish in the same request, so one call yields a render-ready template. Returns 201 with the full template, or 409 identifier_taken.

curl -X POST https://api.pdfkraft.com/api/v1/document_templates \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Invoice",
    "identifier": "my-invoice",
    "edition_mode": "code",
    "output_type": "pdf",
    "body": "<h1>Invoice {{ number }}</h1>",
    "scss_style": "h1 { color: #223; }",
    "sample_data": { "number": "INV-001" },
    "settings": { "page_format": "A4" },
    "publish": true
  }'
PATCH/document_templates/:id— update the draft (PUT is identical)

Partial update: only fields present in the request change. Accepts the same field aliases as create. settings merges over the current draft settings, while settings_draft replaces them wholesale. Add publish: true to publish the updated draft in the same request. Draft edits never affect the published version until you publish.

POST/document_templates/:id/publish

Copies the draft to the published version and sets published_at. Returns 400 empty_template if the draft body is empty. Every publish (from this route, or publish: true on PUT/PATCH) appends a new entry to the template's version history below — nothing is ever overwritten.

GET/document_templates/:id/versions— publish history

Lists every published snapshot, newest first. Pin POST /documents/ /documents/sync to one with template_version so a publish that lands mid-flight can't change documents you already enqueued.

// GET /document_templates/:id/versions
{ "data": [
  { "version": 4, "published_at": "2024-01-03T00:00:00.000Z" },
  { "version": 3, "published_at": "2024-01-02T00:00:00.000Z" },
  { "version": 2, "published_at": "2024-01-01T12:00:00.000Z" },
  { "version": 1, "published_at": "2024-01-01T00:00:00.000Z" }
] }
GET/document_templates/:id/versions/:version— snapshot content

Full content of one published snapshot: body, scss_style, settings, sample_data, ttl, use_forms, and password exactly as they were at that publish. Returns 404 if that version doesn't exist.

POST/document_templates/:id/versions/:version/rollback

Restores that snapshot as the live published and draft state (so a later accidental publish can't undo the rollback), and records the restore itself as a new version — history is append-only, so a rollback to version 3 shows up as a new version 5 with version 3's content, not as a deletion of versions 4 and 5. Returns the updated template plus { rolled_back_to, new_version }.

GET/document_templates/:id/variables— payload introspection

Statically analyzes the template and returns the payload variables it reads — validate your payload contract at integration time instead of shipping blank PDFs. Loop variables are reported against their collection (items[].qty). Analyzes the published body by default; use ?source=draft for the draft. Best-effort: variables used inside snippets are not included.

// GET /document_templates/:id/variables
{
  "source": "published",
  "variables": [
    "customer.name",
    "order.items[].qty",
    "order.items[].sku",
    "order.number"
  ],
  "loops": ["order.items"]
}
POST/document_templates/:id/preview

Renders the current draft with its sample data through the production pipeline. No quota, no webhooks; the file expires after 1 hour. Returns {document_id, download_url, generation_logs}. (To preview with a custom payload, use POST /documents/preview with use_draft: true.)

DELETE/document_templates/:id

Deletes the template. Existing documents are kept (their template_id becomes null). Returns 204.

GET/document_template_cards

Paginated list of templates. Query params: page, folder_id, search.

{
  "data": [
    {
      "id": "uuid",
      "name": "Invoice",
      "identifier": "my-invoice",
      "output_type": "pdf",
      "published_at": "2024-01-01T00:00:00.000Z",
      "created_at": "...",
      "updated_at": "..."
    }
  ],
  "total": 3,
  "page": 1,
  "pages": 1
}
GET/document_templates/:id

Full template including draft body, SCSS, settings, and sample data.

Settings object

The settings object controls page layout. All fields are optional; on PATCH it merges over the current draft settings.

FieldTypeDescription
page_formatenumA4, Letter, or custom.
custom_width_mm / custom_height_mmnumberPage size in millimetres when page_format is custom.
orientationenumportrait (default) or landscape.
margin_top_mm / margin_bottom_mm / margin_left_mm / margin_right_mmnumberPage margins in millimetres, 0–100. Default 10.
header_html / footer_htmlstringHTML fragments repeated at the top/bottom of every PDF page. Rendered in an isolated context — inline your styles, and embed any images as data URIs. Custom @font-face fonts are not supported here (a Chromium limitation on native print headers/footers); the main document body has no such restriction.
image_width / image_heightnumberViewport size in pixels for image output types (png, jpg, webp).
wait_for_jsbooleanWhen the document contains a <script>, rendering waits an extra 2 seconds after page load so client-side JavaScript (charts, QR codes, async data) can finish. Script-free documents are never delayed. Default true; set false to render immediately on load.
emoji_renderingbooleanCurrently disabled regardless of value: it forced color-emoji presentation for dual-presentation characters (❤ ✈ © …), but ASCII digits and #/* share that same Unicode classification (they combine with a keycap mark), so it also corrupted digit rendering — every number in the document — via a broken fallback font. If you need guaranteed color emoji for a symbol, include the U+FE0F presentation selector in your payload text (e.g. ❤️ rather than ); every browser renders that as color by default, with no risk to digits.

Template-level fields

Alongside settings, these top-level fields (set on create or update) change how documents are generated from the template:

FieldTypeDescription
passwordstring | nullDefault password for PDFs generated from this template — applied when the request doesn't send its own password (a request password always wins). Previews are never password-protected.
ttlnumber | nullRetention override in seconds for documents generated from this template. Can only shorten the plan's retention window, never extend it. Useful for short-lived documents like one-time download links.
use_formsbooleanEnables fillable PDF form fields — see below.
folder_idstring | nullFolder assignment — see Folders below.

Fillable PDF forms (AcroForms)

Set use_forms: true on a template to turn elements carrying a data-acrofield attribute into interactive form fields in the generated PDF. Supported types: TEXT, CHECKBOX, DROPDOWN (with OPTIONS:), and DATE (with optional FORMAT:).

<span data-acrofield="TEXT:customer_name"></span>
<span data-acrofield="CHECKBOX:terms_accepted"></span>
<span data-acrofield="DROPDOWN:country OPTIONS:DE,FR,NL"></span>
<span data-acrofield="DATE:signed_on FORMAT:yyyy-mm-dd"></span>

Folders

Templates can be organized into folders (set folder_id on the template). Folders are referenced by key restrictions (folder_ids) and webhook channels (folder-{folderId}). Folder management (GET/POST /folders, PUT/DELETE /folders/:id) requires a dashboard session — it is not available to API keys.

Webhooks

PDFKraft posts a signed payload to your HTTPS endpoint when document generation completes or fails. Register endpoints in the Webhooks dashboard. Endpoint URLs must be publicly reachable http(s) addresses — URLs that resolve to loopback, link-local, or private (RFC 1918) addresses are rejected with 400 invalid_url.

Endpoint management

Method & pathDescription
GET /webhook_endpointsList all endpoints.
POST /webhook_endpointsCreate an endpoint (URL must be public). Returns the signing secret (shown once).
PUT /webhook_endpoints/:idUpdate URL (re-validated), channels, or enabled state.
DELETE /webhook_endpoints/:idDelete an endpoint.
GET /webhook_endpoints/:id/deliveriesDelivery history (100 per page).
POST /webhook_endpoints/:id/deliveries/:delivery_id/replayRe-enqueue a failed delivery.

Channels

Each endpoint subscribes to one or more channels, matched against documents at delivery time.

Channel valueMatches
*Every document event.
template-{templateId}Documents from a specific template UUID.
folder-{folderId}Documents from any template inside a specific folder.
any stringDocuments where meta._webhook_channel equals this string.

Headers sent to your endpoint

HeaderValue
X-PDFKraft-Eventdocuments.generation.success or documents.generation.failure
X-PDFKraft-TimestampUnix epoch seconds (string) at delivery time.
X-PDFKraft-SignatureHMAC-SHA256(secret, "{timestamp}.{body}") hex string.

Payload body

The body is JSON: an event string and the full document card (same shape as the API). The documents.generation.failure event fires once, after all generation attempts are exhausted, and carries the reason in failure_cause / failure_code.

{
  "event": "documents.generation.failure",
  "document": {
    "id": "doc-uuid",
    "status": "failure",
    "filename": "invoice_Alice.pdf",
    "download_url": null,
    "public_share_link": null,
    "output_type": "pdf",
    "document_template_identifier": "my-invoice",
    "failure_cause": "Snippet 'header' not found",
    "failure_code": "snippet_not_found",
    "meta": {},
    "created_at": "2024-01-01T00:00:00.000Z",
    "generated_at": null
  }
}

Signature verification (Node.js)

import crypto from 'crypto'

function verifyWebhook(secret, timestamp, rawBody, signature) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex')
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  )
}

Retry schedule

Failed deliveries (non-2xx response) are retried automatically up to 6 times:

1 min5 min30 min2 h6 h24 h

Errors

All error responses are JSON with an error field; newer endpoints also mirror it as a stable code field — branch on codes, not on message strings, which may change.

Statuserror codeCause
400validation_errorRequest body failed validation. Includes a details object.
401unauthorizedMissing or invalid API key.
403insufficient_scopeThe API key's scope or template restriction does not allow this action (e.g. generate-only key modifying a template, restricted key using another template or raw_html).
404not_foundResource does not exist or belongs to a different account.
400invalid_urlWebhook URL is not a public http(s) address (points to an internal/private host).
400empty_templateTried to publish a template whose draft body is empty.
400template_not_published / template_draft_emptyThe requested template version has no body (e.g. /variables on an unpublished template — add ?source=draft).
409identifier_takenTemplate identifier already in use for this account.
409idempotency_key_in_flightA request with the same Idempotency-Key is still being processed. Retry shortly.
413Payload Too LargeRequest body exceeds 1 MB. Affects large raw_html or inline data-URI images — host large assets at a URL instead (see Images & assets).
422template_parse_failedTemplate body could not be parsed as Liquid (from /variables).
422unresolved_variablesGeneration request had strict_variables: true and the payload leaves at least one template variable unresolved. Includes an unresolved_variables array of the missing paths.
404template_version_not_foundThe template_version pinned on a generation request (or the version in a versions/rollback URL) doesn't exist.
429rate_limitedToo many requests. Check Retry-After header.
429quota_exceededMonthly document quota reached. Upgrade or wait for the next billing cycle.

Generation failure codes

When generation fails, the document card (and webhook payload) carries a failure_code alongside the human-readable failure_cause:

failure_codeMeaning
template_render_failedLiquid rendering failed — check syntax and payload variables.
snippet_not_foundThe template references a snippet that doesn't exist.
style_compile_failedSCSS compilation failed.
template_not_publishedGeneration attempted against a template with no published body.
render_failedThe HTML could not be rendered to the output format.
render_unavailableThe rendering service was temporarily unavailable — safe to retry.
postprocess_failedPDF post-processing (forms, normalization, password) failed.
storage_failedThe generated file could not be stored.
no_inputNeither template nor raw HTML was available at generation time.
template_version_not_foundThe pinned template_version no longer exists.
internal_errorAnything else. Contact support if it persists.
// 400 validation_error
{
  "error": "validation_error",
  "details": {
    "fieldErrors": { "document_template_id": ["Invalid uuid"] },
    "formErrors": []
  }
}

// 429 quota_exceeded
{
  "error": "quota_exceeded",
  "message": "Monthly quota of 20 documents reached",
  "upgrade_url": "https://pdfkraft.com/dashboard/billing"
}

Liquid templating

Template bodies use LiquidJS syntax. Variables from payload are available directly in the template. A missing variable renders as an empty string rather than an error — validate your payload contract up front with GET /document_templates/:id/variables (static, template-pick time) and unresolved_variables on every generation response (dynamic, this specific payload — see Documents) so a template edited after integration doesn't silently start shipping blank fields.

<!-- Variables -->
<p>Hello, {{ customer_name }}!</p>

<!-- Conditionals -->
{% if total > 1000 %}
  <p class="discount">10% loyalty discount applied</p>
{% endif %}

<!-- Loops -->
{% for item in line_items %}
  <tr>
    <td>{{ item.name }}</td>
    <td>{{ item.price }}</td>
  </tr>
{% endfor %}

<!-- Reusable snippets (created in the dashboard) -->
{% render 'company-header' %}

Snippets

Snippets are reusable Liquid fragments (headers, footers, address blocks) included with {% render 'name' %}. They work in template bodies and in raw_html (when a payload is supplied). Names are lowercase letters, digits, and hyphens. Manage them in the dashboard or via GET/POST /snippets and GET/PUT/DELETE /snippets/:id (dashboard session only, body {name, body}). Rendering a document that references a missing snippet fails with failure_code: snippet_not_found.

Images & assets

Documents are rendered by a real browser engine, so templates and raw_html can use images, web fonts, and stylesheets exactly like a web page. There are three ways to get an image into a document:

MethodUse for
Static URL in the templateFixed branding — logos, watermarks, backgrounds. <img src="https://your-cdn.com/logo.png">
URL from the payloadPer-document images such as product photos: put <img src="{{ product.image_url }}"> in the template and send the URL in payload.
Inline data URISmall images sent directly in the request, no hosting needed: <img src="data:image/png;base64,..."> — either hardcoded or passed through the payload.
// Template
<h1>Invoice {{ number }}</h1>
{% for item in items %}
  <img src="{{ item.image_url }}" width="120">
  <p>{{ item.name }} — {{ item.price }}</p>
{% endfor %}

// Payload
{
  "number": "INV-001",
  "items": [
    { "name": "Desk lamp", "price": "€49",
      "image_url": "https://your-cdn.com/products/lamp.jpg" }
  ]
}
  • Asset URLs must be publicly reachable: requests to loopback, link-local, or private (RFC 1918) addresses are blocked by the renderer.
  • The request body is limited to 1 MB, which caps inline data-URI images at roughly 700 KB of image data. For anything larger, host the image and pass its URL.
  • An image URL that is unreachable or returns an error does not fail generation — the document renders with a broken-image placeholder. Verify your URLs.
  • The same rules apply to web fonts and external stylesheets.
  • In page header_html / footer_html, use data URIs — external images are not loaded there.

Plan limits

LimitFreeStarterPro
Documents / month205005,000 + €0.005 overage
Document retention1 day1 day30 days
Sync generation timeout30 s30 s120 s
Public share linksNoNoYes
Trial (new accounts)30-day Pro trial · 300 document cap · No card required