API Reference
Render templates, manage assets, and integrate PDF generation into any stack.
Overview
The pdfs.build API lets you render templates to PDFs, manage your template library, and retrieve render logs — all over HTTPS with JSON request bodies and standard HTTP status codes.
New here? The PDF generation API page covers the model in prose, generating PDFs from a template explains where templates come from, and the comparisons cover migrating from another tool.
https://api.pdfs.build application/pdf Authentication
All API requests require a bearer token. Create and manage API keys from Settings → API Keys in your dashboard. Keys are shown once on creation — store them securely.
The primary public REST API is versioned under /v2/organizations/:organizationId/* and accepts API keys only. API keys are organization-scoped, and the organization in the URL must match the key. The legacy /v1/* API remains supported.
Key format
Keys are prefixed with prs_ and scoped to your organization.
curl https://api.pdfs.build/v2/organizations/org_abc/templates \
-H "Authorization: Bearer prs_your_api_key" /v2/organizations/:organizationId/templates Create template
Creates a draft template owned by the user that minted the API key. Drafts are not visible to the render endpoint until they are published. Template source is normalized through the same publishing pipeline used by the app and should define a top-level #let render(...) = { ... } function whose named parameters match the schema fields.
Request body
| Field | Type | Description |
|---|---|---|
| externalId | string | Your organization-scoped template ID. Required and unique. |
| name | string | Display name. Required. |
| description | string | Optional one-line summary. |
| code | string | Template source. Must define a top-level #let render(...) entry function. Required. |
| schema | object | JSON Schema describing the data payload accepted by the render endpoint. |
| sampleData | object | Example data that satisfies the schema. |
| schemaLocked | boolean | Optional. Set to true at creation to prevent embedded editing from adding, removing, or retyping schema fields. Defaults to false and cannot be changed later. |
| pageSettings | object | Page size, margins, and other layout settings. |
Returns 201 with both the immutable generated internalId and your externalId. V2 URLs use the external ID; v1 continues to use the internal ID.
External IDs are case-sensitive, 1–128 characters, and may contain ASCII letters, digits, ., _, ~, and -. The values . and .. are reserved. IDs remain reserved after a template is deleted.
/v2/organizations/:organizationId/templates/:externalId/publish Publish template
Promotes a draft template to published so it becomes visible to your organization and callable via the render endpoint. The caller must own the template or be an organization admin/owner. Enforces your plan's published-template limit.
Request body (optional)
| Field | Type | Description |
|---|---|---|
| name | string | Rename the template on publish. |
| description | string | Update the description on publish. |
Returns the updated template detail. Returns 402 template_limit_reached if the org's plan does not allow another published template.
/v2/organizations/:organizationId/templates/:externalId/unpublish Unpublish template
Moves a published template back to draft. Once unpublished, the template can no longer be rendered by the public API and is removed from organization template listings. Existing render log entries are preserved.
Returns the updated template detail.
/v2/organizations/:organizationId/templates/:externalId/render Render PDF
Compiles the specified template with the provided data payload and returns a binary PDF. Data is validated against the template's schema before rendering.
Request body
| Field | Type | Description |
|---|---|---|
| data | object | Key-value pairs matching the template's schema. |
| version | number or string | Optional. Pin a version number (3) or a channel name ("staging"). Omit it to serve whatever is promoted to latest. |
Versions
Publishing freezes a template into an immutable version and points the latest channel at it. Later edits stay private until you promote them, so production keeps rendering the same document — and the same schema — while you iterate. Freeze the draft as a new version to test it ({ "version": 4 }) while production stays on the promoted one, then promote when you are ready and every caller that omits version picks it up. Promoting an older version back is an instant rollback.
Response
Returns Content-Type: application/pdf on success. On error, returns JSON with an error object.
const response = await fetch(
'https://api.pdfs.build/v2/organizations/org_abc/templates/invoice-primary/render',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
data: {
company: 'Acme Corp',
items: [
{ name: 'Consulting', qty: 3, price: 150 },
],
due_date: '2025-06-01',
}
}),
}
);
const pdf = await response.arrayBuffer(); /v2/organizations/:organizationId/templates/:externalId/render Async renders
Add "async": true to the render request body to render in the background instead of waiting for the PDF. The API answers 202 Accepted immediately with a job handle; you then wait for the render.completed webhook or poll the job status. Omitting async (or sending false) keeps the synchronous behavior unchanged, and async renders count toward the same monthly quota as synchronous renders.
Request body
| Field | Type | Description |
|---|---|---|
| data | object | Key-value pairs matching the template's schema. |
| async | boolean | Set to true to render in the background. Defaults to false (synchronous). |
| webhookIds | string[] | Optional. Restrict this render's webhook events to the given registered endpoints. |
Response
Returns 202 Accepted with the job handle. The quota admission check happens at enqueue; finalization happens when the job completes.
{
"id": "job_7a1b...",
"status": "queued",
"statusUrl": "https://api.pdfs.build/v2/organizations/org_abc/renders/job_7a1b..."
} Polling for the result
| Endpoint | Description |
|---|---|
| GET /v2/organizations/:organizationId/renders/:jobId | Job status — queued, processing, success, or error — plus durationMs, error, and downloadUrl once finished. |
| GET /v2/organizations/:organizationId/renders/:jobId/pdf | The rendered PDF (application/pdf). Returns 409 while the job is not finished and 404 for unknown jobs. |
{
"id": "job_7a1b...",
"status": "success",
"templateExternalId": "invoice-primary",
"durationMs": 123,
"downloadUrl": "https://api.pdfs.build/v2/organizations/org_abc/renders/job_7a1b.../pdf",
"createdAt": "2026-08-30T12:00:00Z",
"completedAt": "2026-08-30T12:00:01Z"
}
The downloadUrl requires the same API key (Authorization: Bearer prs_...). The legacy POST /v1/render route remains sync-only.
/v2/organizations/:organizationId/templates/:externalId/batches Batch renders
Render up to 500 documents from one template in a single request. Every item becomes an async render job, all against the same template version: the version is resolved when you submit, so promoting a channel mid-batch does not mix versions. The exception is a template that renders its live draft (nothing promoted yet, or a legacy auto-publish template): the draft is not pinned, so edits made mid-batch can reach later items. Items send no per-render webhooks; one batch.completed event goes out when every item has succeeded or failed (webhooks need the Pro plan or higher).
Request body
| Field | Type | Description |
|---|---|---|
| items | object[] | 1 to 500 entries, each {"data": ...} matching the template's schema. |
| webhookIds | string[] | Optional. Endpoints to notify with batch.completed. Omitted: every enabled endpoint. Empty: none. |
| version | integer | string | Optional. Same selector as a single render. |
Response
Returns 202 Accepted with id, status, total and statusUrl. The whole batch is admitted against your monthly quota at once: if it does not fit, the request fails with 429 and nothing is queued. Only successful items are charged. More than 500 items returns 422 invalid_batch_size; the body may be up to 10 MB.
Polling for the result
GET /v2/organizations/:organizationId/batches/:batchId returns the batch's status (processing or completed), succeeded, failed and pending counts, and every item in submission order as { index, jobId, status, downloadUrl?, error? }. Each downloadUrl is that item's render job PDF route and needs the same API key.
/v2/organizations/:organizationId/templates List templates
Returns published templates in the API key's organization. Drafts created by the key owner remain available through the single-template endpoint.
[
{
"internalId": "550e8400-e29b-41d4-a716-446655440000",
"externalId": "invoice-primary",
"name": "Invoice v3",
"description": "Standard invoice with line items",
"status": "published",
"createdAt": "2025-03-12T10:24:00Z",
"updatedAt": "2025-04-01T08:15:00Z"
}
] /v2/organizations/:organizationId/templates/:externalId Get template
Returns a single template's metadata, schema definition, sample data, and schemaLocked state. Use the schema field to know which keys your data payload must include when rendering.
{
"internalId": "550e8400-e29b-41d4-a716-446655440000",
"externalId": "invoice-primary",
"name": "Invoice v3",
"status": "published",
"schemaLocked": true,
"schema": {
"type": "object",
"properties": {
"company": { "type": "string" },
"due_date": { "type": "string", "format": "date" },
"items": { "type": "array" }
}
},
"sampleData": { /* ... */ }
} /v2/organizations/:organizationId/templates/:externalId Rename external ID
Send {"externalId":"invoice-primary"}. The old v2 URL stops resolving immediately; the internal ID, v1 URL, and historical render-log foreign keys do not change. Requires template ownership or organization admin access.
/v2/organizations/:organizationId/logs Render logs
Returns a paginated list of PDF render events for your organization. Each log exposes templateInternalId and the templateExternalId captured when that render occurred, so later renames do not rewrite audit history. Use GET /v2/organizations/:organizationId/logs/:logId for detail or GET /v2/organizations/:organizationId/templates/:externalId/logs for one template.
Query parameters
| Parameter | Type | Description |
|---|---|---|
| limit | integer | Max results to return (default: 50) |
| offset | integer | Pagination offset |
| status | string | Filter by status: success or error |
| source | string | Filter by source: api, ui, or mcp |
| externalId | string | Filter by the external ID captured at generation time. Historical IDs continue to work after a rename. |
| search | string | Full-text search on template name |
| from | string (ISO 8601) | Start of time range |
| to | string (ISO 8601) | End of time range |
Webhooks
Outbound webhooks notify your systems when API renders complete or fail — both synchronous renders and async render jobs. UI, MCP, and other in-app renders do not emit webhook events. Webhooks are available on the Pro plan and higher.
Register endpoints from Developers → Webhooks in the dashboard, over MCP (create_webhook and friends), or through the v2 API directly — all three surfaces manage the same endpoints with the same plan gating. An organization can register multiple endpoints, and every enabled endpoint receives every render event. To restrict a single render's events to specific endpoints, pass their ids in the optional webhookIds field on the render endpoint — ids must reference existing, enabled endpoints of the same organization, and unknown ids return 422. Webhook delivery never affects render responses.
Managing endpoints via the API
Create, rotate-secret, and test require the Pro plan or higher; list, update, delete, and deliveries stay open. Endpoint URLs must be public http(s) destinations — non-global addresses (loopback, private, link-local, CGNAT, and other reserved ranges) are rejected with 400 url_not_allowed at registration and re-validated before every delivery.
| Endpoint | Plan gate | Description |
|---|---|---|
| GET /v2/organizations/:organizationId/webhooks | — | List endpoints (secrets are never returned) |
| POST /v2/organizations/:organizationId/webhooks | Pro+ | Create with { url, description?, enabled? }; returns the endpoint and the signing secret once |
| PATCH /v2/organizations/:organizationId/webhooks/:id | — | Update url / description / enabled |
| DELETE /v2/organizations/:organizationId/webhooks/:id | — | Soft delete |
| POST /v2/organizations/:organizationId/webhooks/:id/rotate-secret | Pro+ | New signing secret, shown once |
| POST /v2/organizations/:organizationId/webhooks/:id/test | Pro+ | Enqueue a webhook.test event |
| GET /v2/organizations/:organizationId/webhooks/:id/deliveries | — | Recent 50 deliveries |
Events
| Type | When |
|---|---|
| render.completed | API render finished successfully (sync or async) |
| render.failed | API render failed (compilation error, upload failure, crashed async job) |
| batch.completed | Every item of a batch render has succeeded or failed (data: batchId, total, succeeded, failed) |
| webhook.test | Test event sent from the dashboard |
Payload
{
"id": "evt_9f2c...",
"type": "render.completed",
"createdAt": "2026-08-30T12:00:00Z",
"data": {
"renderId": "d4e5...",
"jobId": "job_7a1b...",
"organizationId": "org_...",
"templateExternalId": "invoice-primary",
"status": "success",
"durationMs": 123,
"errorMessage": null,
"downloadUrl": "https://api.pdfs.build/v2/organizations/org_.../renders/job_7a1b.../pdf"
}
} id is unique per delivery — use it for consumer-side idempotency. jobId and downloadUrl are present for async renders only; the download URL requires the same API key. render.failed events set status: "error" and errorMessage.
Verifying signatures
Each delivery is signed with the endpoint secret using the Standard Webhooks scheme, so you can verify it with any svix-compatible library. The signing secret (whsec_...) is shown once when the endpoint is created.
| Header | Description |
|---|---|
| webhook-id | The event id (evt_...). |
| webhook-timestamp | Unix seconds when the delivery was sent. |
| webhook-signature | v1,<base64 HMAC-SHA256> over ${webhook-id}.${webhook-timestamp}.${raw-body}, keyed by the base64 portion of the secret. |
Acknowledgement and retries
Respond with any 2xx within 5 seconds; anything else — including timeouts — counts as a failed attempt and is retried with capped exponential backoff: up to 10 attempts, min(60, 2^attempts) minutes between attempts. A delivery is marked failed after the 10th attempt. You can inspect recent deliveries and send test events from the dashboard.
Migrate from v1
V1 remains supported and continues to identify templates by the generated internal ID. V2 adds an organization-scoped external ID that you control. Existing templates initially use their internal ID as the external ID; rename it in the dashboard or with PATCH /v2/organizations/:organizationId/templates/:externalId.
| V1 | V2 |
|---|---|
| POST /v1/render | POST /v2/organizations/:organizationId/templates/:externalId/render |
Send templateId and data | Move the template identifier into the URL and send only data |
| Generated internal ID | Client-managed external ID, unique within the organization |
Importing a .docx
An existing Word document can be converted into a template instead of being rebuilt by hand. The import reads the document's structure and produces template code plus a matching JSON Schema, which is then editable like any other template — including through the chat agent.
Available in the app under Templates → Import,
and over MCP with
import_docx,
process_docx_import
and
convert_docx_to_template.
Documents whose structure is expressed with real Word styles — headings, tables, lists — import most cleanly, because those are what map onto template structure. A file formatted by hand, with spacing and alignment instead of styles, carries no structure to read and is usually faster to redraw from one of the starting designs.
MCP server (for AI agents)
The same authentication layer also serves a Model Context Protocol endpoint. Agents that connect to it can design, publish, and render PDFs inside a single conversation — without API keys or dashboard handoffs.
https://backend.pdfs.build/mcp Tool surface
The MCP server exposes the entire template lifecycle plus authenticated rendering. MCP renders count toward the same monthly PDF render quota as UI, authenticated form, automation, and REST API renders.
| Tool | Purpose |
|---|---|
| list_templates / get_template / browse_templates | Discover templates and load their code, schema, and sample data. |
| create_template / duplicate_template | Start a fresh draft or clone an existing template. |
| write_document / edit_document / write_schema / write_sample_data | Author the template code, schema, and data. |
| compile_document | Compile the active template against default edge fixtures and optional custom fixtures, returning per-fixture diagnostics. |
| render_template | Render a saved template without an API key. Pass public_share=true to create share/download URLs, and expires_in_days to customize the expiry. |
| save_template | Persist the active session's changes after the compile gate passes; draft WIP saves can explicitly skip the gate. |
| publish_template / unpublish_template | Move templates between draft and published. Publishing requires the compile gate to pass. |
| create_api_key / list_api_keys / delete_api_key | Mint, audit, and revoke REST API keys for external integrations. |
| list_webhooks / create_webhook / update_webhook / delete_webhook / rotate_webhook_secret / test_webhook / list_webhook_deliveries | Manage webhook endpoints (Pro plan and higher) and debug deliveries. Endpoint ids (whe_...) are what the REST API async render's webhookIds field targets. |
| document_reference | Look up template code reference inline. |
| list_fonts / upload_font / confirm_font_upload | Browse org fonts and upload custom TTF/OTF files. |
| search_google_fonts / install_google_font | Browse Google Fonts and install a family into the org. |
Sessions and parallel agents
Each MCP connection gets its own isolated session on initialize, holding one active template (the one last loaded with get_template or created with create_template). Editing tools like edit_document and save_template operate on that session's active template.
Every editing tool takes a templateId and is refused when it does not match the session's active template, so a write can never land on the wrong document.
To build many templates in parallel, give each agent its own MCP connection — parallel agents then work on separate templates with fully independent state. Do not fan multiple agents out over a single shared connection: their calls run one at a time against one active template, so whichever agent loads a template last makes every other agent's calls fail. If two sessions do end up editing the same template, save_template detects the external modification and refuses to silently overwrite it — reload with get_template and re-apply your changes.
Connect from Claude Desktop, Cursor, or any MCP-compatible client. See the MCP integration guide for an end-to-end example.
Errors
All errors return a JSON body whose top-level error is a string — a machine-readable code for most failures — usually alongside a human-readable message. Validation and compilation failures instead carry a structured details or diagnostics array.
{
"error": "draft_not_renderable",
"message": "Only published templates can be rendered via the API. Publish this template first."
} | Status | Code | Description |
|---|---|---|
| 400 | invalid_request | Missing or malformed request parameters |
| 400 | invalid_external_id | External ID is not URL-safe or exceeds 128 characters |
| 400 | already_published | Trying to publish a template that is already published |
| 400 | already_draft | Trying to unpublish a template that is already a draft |
| 401 | unauthorized | Missing or invalid API key |
| 403 | forbidden | Caller does not have permission to publish/unpublish this template |
| 403 | organization_scope_mismatch | Organization URL does not match the API key organization |
| 403 | draft_not_renderable | The template is a draft. Only published templates render over the API. |
| 409 | external_id_conflict | External ID is already reserved in the organization |
| 404 | not_found | Template not found or not accessible |
| 402 | template_limit_reached | Org plan's published-template limit is reached. Unpublish a template or upgrade the plan. |
| 402 | api_renders_not_allowed_on_free | The REST render API is not available on the Free plan. Use the app, authenticated forms, MCP rendering, or upgrade to Starter or higher. |
| 400 | Validation failed | Data payload doesn't match the template schema. Carries details[] rather than a message. |
| 422 | Compilation failed | The template failed to compile with this data. Carries diagnostics[] rather than a message. |
| 429 | render_quota_exceeded | This billing period's PDF render quota is used up — see Rate limits |
| 500 | render_failed | Internal rendering error |
Rate limits
Requests are not throttled per API key. The limit that applies is your plan's monthly PDF render quota, shared by every render surface — REST API, app, MCP and the embedded editor. Once it is used up, render requests return 429 with render_quota_exceeded until the billing period rolls over or you buy a top-up pack; async renders are counted when they are queued.
Template management, logs and webhook endpoints are not metered.
API keys
API keys are scoped to your organization and grant access to all templates your organization can see. Keys are prefixed with prs_ and displayed only once on creation — copy them immediately and store them in a secret manager or environment variable.
Manage your API keys from Settings → API Keys in the dashboard. You can create multiple keys for different environments and revoke them individually.