Generate a PDF

POST /v1/generate

Renders one of your templates with the supplied data and returns a PDF. Requires a valid API key.

Request body

FieldTypeDescription
template_idstringRequired. The id of a template you own.
dataobjectThe values merged into the template's placeholders (max 256 KB).
options.outputstring"binary" (default) returns the PDF bytes; "url" returns a 24-hour signed URL.
options.strictbooleanWhen true, return 422 missing_data_fields instead of rendering if the template references fields not present in data. Default false (absent fields render blank).
options.validatebooleanWhen true, validate data against the template's typed contract before rendering; an invalid payload returns 422 invalid_template_data with field-level errors. Default false.
options.formatstring"A4", "A6", or "Letter". Overrides the template's page format for this request.
options.landscapebooleanLandscape orientation. Overrides the template default. Default false (portrait).
options.marginsobjectOptional top/right/bottom/left as unit strings (mm, in, px). Converted to mm and bounded 0–50mm. Default 16mm all sides.
options.metadataobjectPDF metadata: title, author, subject (≤256 chars each), keywords (≤20 strings, ≤64 chars each).
options.filenamestringASCII-safe download name ending in .pdf (e.g. invoice-1042.pdf). Sets Content-Disposition (binary) or the signed URL download name (url mode).
options.watermarkobjectCustom diagonal stamp: { "text": "DRAFT", "opacity": 0.12 }. Paid plans only — free returns 403 watermark_not_allowed. Test keys always show the TEST stamp instead.

Example

curl -X POST https://doclinth.com/v1/generate \
  -H "Authorization: Bearer dl_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "template_id": "tmpl_…", "data": { "total": 3078 } }' \
  --output invoice.pdf

Render options (format, landscape, margins, metadata, filename)

Per-request page settings override the template defaults for that call only. Unknown options keys return 422 invalid_options.

curl -X POST https://doclinth.com/v1/generate \
  -H "Authorization: Bearer dl_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "tmpl_…",
    "data": { "total": 3078 },
    "options": {
      "format": "Letter",
      "landscape": false,
      "margins": { "top": "20mm", "right": "16mm", "bottom": "20mm", "left": "16mm" },
      "metadata": {
        "title": "Invoice 1042",
        "author": "Acme Billing",
        "subject": "Customer invoice",
        "keywords": ["invoice", "acme"]
      },
      "filename": "invoice-1042.pdf"
    }
  }' \
  --output invoice-1042.pdf

Custom watermark (paid plans)

Free plans keep the forced "Generated with doclinth" badge and cannot set options.watermark. Paid plans may stamp arbitrary text:

curl -X POST https://doclinth.com/v1/generate \
  -H "Authorization: Bearer dl_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "tmpl_…",
    "data": { "total": 3078 },
    "options": { "watermark": { "text": "DRAFT", "opacity": 0.12 } }
  }' \
  --output draft.pdf

Node / TypeScript

import fs from "node:fs";

const res = await fetch("https://doclinth.com/v1/generate", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.DOCLINTH_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ template_id: "tmpl_…", data: { total: 3078 } }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
await fs.promises.writeFile("invoice.pdf", Buffer.from(await res.arrayBuffer()));

The official @doclinth/sdk package wraps this API with types for every template — see packages/sdk.

Python

import os, requests

res = requests.post(
    "https://doclinth.com/v1/generate",
    headers={"Authorization": f"Bearer {os.environ['DOCLINTH_API_KEY']}"},
    json={"template_id": "tmpl_…", "data": {"total": 3078}},
)
res.raise_for_status()
open("invoice.pdf", "wb").write(res.content)

PHP (Laravel)

use Illuminate\Support\Facades\Http;

$res = Http::withToken(env('DOCLINTH_API_KEY'))
    ->post("https://doclinth.com/v1/generate", [
        'template_id' => 'tmpl_…',
        'data' => ['total' => 3078],
    ])
    ->throw();

file_put_contents('invoice.pdf', $res->body());

Ruby

require "net/http"
require "json"

res = Net::HTTP.post(
  URI("https://doclinth.com/v1/generate"),
  { template_id: "tmpl_…", data: { total: 3078 } }.to_json,
  "Authorization" => "Bearer #{ENV.fetch('DOCLINTH_API_KEY')}",
  "Content-Type" => "application/json",
)
File.binwrite("invoice.pdf", res.body)

Async generation & webhooks

For batch or slow renders, add options.webhook_url (a public https endpoint). The API responds immediately with 202 and a generation_id, then renders in the background and POSTs the signed result to your URL:

{ "template_id": "tmpl_…", "data": { … },
  "options": { "output": "url", "webhook_url": "https://you.example.com/hooks/pdf" } }
// → 202 { "generation_id": "…", "status": "queued" }

The webhook body your endpoint receives:

{ "generation_id": "…", "status": "success",
  "url": "https://…/signed.pdf", "expires_at": "2026-07-01T12:00:00.000Z" }
// on failure: { "generation_id": "…", "status": "error", "error": { "code", "message" } }

Each delivery is signed with an X-Doclinth-Signature: sha256=<hex> header: HMAC-SHA256 of the raw request body using your webhook signing secret (create one in dashboard Settings). Verify it before trusting the payload. Async requests still consume one quota unit (refunded automatically if the render fails).

Retrieve a generation

GET /v1/generations/{id}

Look up a past generation by the id from its X-Request-Id header (for async requests, the generation_id returned with the 202). It returns status metadata only — async results are delivered to your webhook; the PDF is not re-downloadable here.

curl https://doclinth.com/v1/generations/GENERATION_ID \
  -H "Authorization: Bearer dl_live_YOUR_KEY"

{
  "id": "…",
  "status": "success",
  "error_code": null,
  "duration_ms": 1840,
  "created_at": "2026-07-03T12:00:00.000Z",
  "warnings": []
}

A generation queued via webhook has no record until the render finishes, so it returns 404 generation_not_found until then.

Idempotency

To make a request safe to retry (e.g. after a network timeout), send an Idempotency-Key header with a unique value you generate; a UUID works well:

-H "Idempotency-Key: 5f3b…-a1"

If a request with the same key arrives again within 24 hours, the API responds 409 duplicate_request instead of generating (and billing) a second PDF. A key is released if the request fails, so an idempotent retry after a 5xx succeeds. Use a fresh key for each distinct document. One exception: a repeat of an output:"url"request returns the original document's URL again (200 with replayed: true) rather than 409; binary repeats still 409.

Responses

Binary (default). 200 OK with Content-Type: application/pdf; the response body is the PDF.

Signed URL. With { "options": { "output": "url" } }:

{ "url": "https://…/signed.pdf", "expires_at": "2026-07-01T12:00:00.000Z" }

Response headers. Every response — success, error, or 202 — carries X-Request-Id: a unique id for that request. Quote it in support; it is the same id shown in your dashboard Generation logs. When a render completes but the template referenced fields missing from data, the unresolved paths are returned as an X-Doclinth-Warnings: missing=field1,field2 header (binary responses) or a "warnings": ["field1", "field2"] array (URL JSON).

Strict mode. Send { "options": { "strict": true } } to reject partial renders: if any field is missing, the response is 422 missing_data_fields listing the unresolved paths, and no quota is consumed. To see exactly which fields a template expects, fetch it with GET /v1/templates/{id} — its sample_data is a working example payload.

Notes

  • Typical render time is ~2 seconds. Requests time out at 20s (504 render_timeout).
  • Each successful generation counts toward your monthly quota. Exceeding it returns 402 quota_exceeded.
  • Templates only format data: compute business values (subtotal, tax, total) on your side and pass them in. See Template syntax.
  • Generation always renders the published version of a template. Editing and saving a draft in the dashboard never changes live output until you click Publish, so you can iterate safely on production templates.
  • doclinth stores no document data: generation logs keep metadata only (status, duration, size).

Full status/code list: Errors. Machine-readable API definition: OpenAPI spec.