{"openapi":"3.1.0","info":{"title":"PDF4.dev API","version":"1.0.0","description":"Generate PDFs from HTML templates with Handlebars variables. Create templates in the dashboard, then render them via API with dynamic data.\n\n## Authentication\n\nAll API requests require a Bearer token. Create an API key in **Settings** and include it in the `Authorization` header:\n\n```\nAuthorization: Bearer p4_live_xxx...\n```\n\n## Quick Start\n\n1. Create a template in the dashboard or via API\n2. Generate an API key in Settings\n3. Call `POST /api/v1/render` with your template ID and data\n\n```bash\ncurl -X POST https://pdf4.dev/api/v1/render \\\n  -H \"Authorization: Bearer p4_live_xxx\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"template_id\": \"invoice\", \"data\": {\"company\": \"Acme\"}}' \\\n  --output invoice.pdf\n```"},"servers":[{"url":"https://pdf4.dev","description":"Current server"}],"security":[{"bearerAuth":[]}],"paths":{"/api/v1/render":{"post":{"operationId":"renderPdf","summary":"Generate a PDF","description":"Render a PDF from a saved template or raw HTML. Pass `template_id` to use a saved template, or `html` for one-off renders. Variables in `{{handlebars}}` syntax are replaced with values from `data`. Returns the PDF as binary data (`application/pdf`).","tags":["Render"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderRequest"},"examples":{"with_template":{"summary":"Render from a saved template","value":{"template_id":"invoice","data":{"company_name":"Acme Corp","invoice_number":"INV-2025-001","total":"$4,500.00"}}},"with_html":{"summary":"Render from raw HTML","value":{"html":"<h1>Hello {{name}}</h1><p>Your order #{{order_id}} is confirmed.</p>","data":{"name":"John","order_id":"12345"}}},"with_format":{"summary":"Custom page format","value":{"html":"<h1>Landscape Report</h1>","format":{"preset":"a4-landscape","margins":{"top":"10mm","bottom":"10mm","left":"15mm","right":"15mm"},"background_color":"#ffffff","font_family":"Inter, sans-serif","font_size":"14px"}}}}}}},"responses":{"200":{"description":"PDF generated successfully","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}},"headers":{"Content-Disposition":{"schema":{"type":"string"},"example":"inline; filename=\"document.pdf\""}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/templates":{"get":{"operationId":"listTemplates","summary":"List templates","description":"Returns all templates belonging to the authenticated user. Requires `full_access` API key scope.","tags":["Templates"],"responses":{"200":{"description":"Array of templates","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Template"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"operationId":"createTemplate","summary":"Create a template","description":"Create a new template. The `name` is required and a URL-safe `slug` is auto-generated from it. Requires `full_access` API key scope.","tags":["Templates"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTemplateRequest"},"example":{"name":"Monthly Invoice","html":"<!DOCTYPE html><html><body><h1>Invoice #{{invoice_number}}</h1><p>Amount: {{total}}</p></body></html>","sample_data":{"invoice_number":"INV-001","total":"$1,000.00"},"pdf_format":{"preset":"a4","margins":{"top":"20mm","bottom":"20mm","left":"15mm","right":"15mm"}}}}}},"responses":{"201":{"description":"Template created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Template"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/templates/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Template ID (`tmpl_xxx`) or slug (e.g. `monthly-invoice`)"}],"get":{"operationId":"getTemplate","summary":"Get a template","description":"Retrieve a single template by ID or slug. Requires `full_access` API key scope.","tags":["Templates"],"responses":{"200":{"description":"Template found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Template"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"put":{"operationId":"updateTemplate","summary":"Update a template","description":"Update a template's HTML, format, or sample data. Only provided fields are updated. Requires `full_access` API key scope.","tags":["Templates"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTemplateRequest"},"example":{"html":"<h1>Updated Invoice #{{number}}</h1>","sample_data":{"number":"INV-002"}}}}},"responses":{"200":{"description":"Template updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Template"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"operationId":"deleteTemplate","summary":"Delete a template","description":"Permanently delete a template. This action cannot be undone. Requires `full_access` API key scope.","tags":["Templates"],"responses":{"200":{"description":"Template deleted","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}}},"example":{"deleted":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/components":{"get":{"operationId":"listComponents","summary":"List components","description":"Returns all components (headers, footers, blocks). Use header/footer IDs in templates for repeating page elements. Block components are referenced via <pdf4-block> tags in template HTML. Requires `full_access` API key scope.","tags":["Components"],"parameters":[{"name":"type","in":"query","description":"Filter by component type","schema":{"type":"string","enum":["header","footer","block"]}}],"responses":{"200":{"description":"Array of components","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Component"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"operationId":"createComponent","summary":"Create a component","description":"Create a header or footer component. Use {{variables}} for data. In footers, use <span class=\"pageNumber\"></span> and <span class=\"totalPages\"></span> for page numbers. Requires `full_access` API key scope.","tags":["Components"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateComponentRequest"}}}},"responses":{"201":{"description":"Component created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Component"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/components/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Component ID (comp_xxx)"}],"get":{"operationId":"getComponent","summary":"Get a component","description":"Retrieve a single component. Requires `full_access` API key scope.","tags":["Components"],"responses":{"200":{"description":"Component found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Component"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"put":{"operationId":"updateComponent","summary":"Update a component","description":"Update a component's name or HTML. Requires `full_access` API key scope.","tags":["Components"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateComponentRequest"}}}},"responses":{"200":{"description":"Component updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Component"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"operationId":"deleteComponent","summary":"Delete a component","description":"Permanently delete a component. Templates using it will no longer show it. Requires `full_access` API key scope.","tags":["Components"],"responses":{"200":{"description":"Component deleted","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/templates/{id}/duplicate":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Template ID to duplicate"}],"post":{"operationId":"duplicateTemplate","summary":"Duplicate a template","description":"Creates a copy of the template with \"(copy)\" appended to its name. Requires `full_access` API key scope.","tags":["Templates"],"responses":{"201":{"description":"Template duplicated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Template"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/logs":{"get":{"operationId":"getLogs","summary":"List PDF generation logs","description":"Returns logs of all PDF generations with cursor-based pagination. Filter by status, template, or date range. Requires `full_access` API key scope.","tags":["Logs"],"parameters":[{"name":"limit","in":"query","description":"Results per page (max 100)","schema":{"type":"integer","default":50,"maximum":100}},{"name":"cursor","in":"query","description":"Cursor for pagination. Use `next_cursor` from a previous response to fetch the next page.","schema":{"type":"string"}},{"name":"status","in":"query","description":"Filter by generation status","schema":{"type":"string","enum":["success","error"]}},{"name":"template_id","in":"query","description":"Filter by template ID","schema":{"type":"string"}},{"name":"from","in":"query","description":"Start date (ISO 8601)","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"End date (ISO 8601)","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Logs with cursor pagination","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CursorPaginatedLogs"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/stats":{"get":{"operationId":"getStats","summary":"Get usage statistics","description":"Returns aggregated usage statistics including template count, total renders, success rate, average duration, and total size. Optionally filter by time period. Session auth only.","tags":["Stats"],"parameters":[{"name":"period","in":"query","description":"Time period filter. Omit for all-time statistics.","schema":{"type":"string","enum":["1h","24h","7d","30d"]}}],"responses":{"200":{"description":"Usage statistics","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Stats"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/api-keys":{"get":{"operationId":"listApiKeys","summary":"List API keys","description":"Returns all API keys for the authenticated user. Only key prefix is shown, not the full token. Session auth only.","tags":["Api keys"],"responses":{"200":{"description":"Array of API keys","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"operationId":"createApiKey","summary":"Create an API key","description":"Create a new API key. The full token is returned only once in the response: store it securely. Session auth only.","tags":["Api keys"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Display name for the key","example":"Production"},"permission":{"type":"string","enum":["full_access","render_only"],"description":"Permission scope. Defaults to full_access.","example":"full_access"}}}}}},"responses":{"201":{"description":"API key created. The `key` field contains the full token: this is the only time it is shown.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ApiKey"},{"type":"object","properties":{"key":{"type":"string","description":"Full API key token (shown only once)","example":"p4_live_abc123..."}}}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/api-keys/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"API key ID"}],"delete":{"operationId":"deleteApiKey","summary":"Delete an API key","description":"Permanently revoke an API key. Any requests using this key will immediately fail. Session auth only.","tags":["Api keys"],"responses":{"200":{"description":"API key deleted","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}}},"example":{"deleted":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/account":{"delete":{"operationId":"deleteAccount","summary":"Delete account","description":"Permanently delete the user account and all associated data (templates, API keys, logs). This action cannot be undone. Session auth only.","tags":["Account"],"responses":{"200":{"description":"Account deleted","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}}},"example":{"deleted":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API key from Settings page. Keys start with `p4_live_` prefix."}},"schemas":{"RenderRequest":{"type":"object","description":"Either `template_id` or `html` is required. If both are provided, `template_id` takes precedence.","properties":{"template_id":{"type":"string","description":"Template ID (`tmpl_xxx`) or slug. The template must belong to the authenticated user.","example":"invoice"},"html":{"type":"string","description":"Raw HTML string. Supports Handlebars `{{variables}}` syntax. Use this for one-off renders without saving a template.","example":"<h1>Hello {{name}}</h1>"},"data":{"type":"object","additionalProperties":true,"description":"Data to replace `{{variables}}` in the template. Supports strings, numbers, booleans, nested objects, and arrays (for `{{#each}}` blocks).","example":{"name":"World","company":"Acme Corp"}},"format":{"$ref":"#/components/schemas/PdfFormat"}}},"Template":{"type":"object","properties":{"id":{"type":"string","description":"Unique template ID","example":"tmpl_a1b2c3d4e5f6"},"name":{"type":"string","description":"Display name","example":"Invoice"},"slug":{"type":"string","description":"URL-safe identifier, unique per user. Can be used as `template_id` in render requests.","example":"invoice"},"html":{"type":"string","description":"HTML template with Handlebars variables"},"plain_text":{"type":"string","description":"Plain text version of the template"},"pdf_format":{"$ref":"#/components/schemas/PdfFormat"},"sample_data":{"type":"object","additionalProperties":true,"description":"Default sample values for preview. These are NOT used during API rendering: you must pass `data` explicitly."},"header_component_id":{"type":"string","nullable":true,"description":"Header component ID (comp_xxx). Repeats on every page when rendering."},"footer_component_id":{"type":"string","nullable":true,"description":"Footer component ID (comp_xxx). Repeats on every page when rendering."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"CreateTemplateRequest":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Template name. A slug is auto-generated from this."},"html":{"type":"string","description":"HTML template content. Supports Handlebars `{{variables}}`."},"pdf_format":{"$ref":"#/components/schemas/PdfFormat"},"sample_data":{"type":"object","additionalProperties":true,"description":"Default sample values for preview"},"header_component_id":{"type":"string","nullable":true,"description":"Header component ID (comp_xxx)"},"footer_component_id":{"type":"string","nullable":true,"description":"Footer component ID (comp_xxx)"}}},"UpdateTemplateRequest":{"type":"object","description":"Only provided fields are updated.","properties":{"name":{"type":"string"},"html":{"type":"string"},"plain_text":{"type":"string"},"pdf_format":{"$ref":"#/components/schemas/PdfFormat"},"sample_data":{"type":"object","additionalProperties":true},"header_component_id":{"type":"string","nullable":true,"description":"Header component ID or null to remove"},"footer_component_id":{"type":"string","nullable":true,"description":"Footer component ID or null to remove"}}},"Component":{"type":"object","properties":{"id":{"type":"string","description":"Unique component ID","example":"comp_a1b2c3d4e5f6"},"name":{"type":"string","description":"Display name","example":"Company header"},"type":{"type":"string","enum":["header","footer","block"],"description":"Component type. Headers repeat at the top of every page, footers at the bottom, blocks render inline where placed."},"html":{"type":"string","description":"HTML with Handlebars variables"},"preview":{"type":"string","description":"Plain text preview (auto-generated from HTML if omitted)"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"CreateComponentRequest":{"type":"object","required":["name","type"],"properties":{"name":{"type":"string","description":"Component name (e.g. Company header)"},"type":{"type":"string","enum":["header","footer","block"],"description":"Component type. Headers repeat on every page (via <thead>), footers repeat on every page (via <tfoot>), blocks render inline where placed."},"html":{"type":"string","description":"HTML with {{handlebars}} variables"},"preview":{"type":"string","description":"Plain text preview (auto-generated from HTML if omitted)"}}},"UpdateComponentRequest":{"type":"object","description":"Only provided fields are updated.","properties":{"name":{"type":"string"},"type":{"type":"string","enum":["header","footer","block"],"description":"Change component type. Headers repeat on every page (via <thead>), footers repeat on every page (via <tfoot>), blocks render inline."},"html":{"type":"string"},"preview":{"type":"string","description":"Plain text preview (auto-generated from HTML if omitted)"}}},"PdfFormat":{"type":"object","description":"Page format configuration. If omitted, defaults to A4 portrait with 20mm top/bottom and 15mm left/right margins.","properties":{"preset":{"type":"string","enum":["a4","a4-landscape","letter","letter-landscape","square","custom"],"description":"Page size preset. Use `custom` with `width`/`height` for arbitrary dimensions.","example":"a4"},"width":{"type":"string","description":"Custom width (only used when preset is `custom`)","example":"210mm"},"height":{"type":"string","description":"Custom height (only used when preset is `custom`)","example":"297mm"},"margins":{"type":"object","description":"Page margins","properties":{"top":{"type":"string","example":"20mm"},"bottom":{"type":"string","example":"20mm"},"left":{"type":"string","example":"15mm"},"right":{"type":"string","example":"15mm"}}},"background_color":{"type":"string","description":"Page background color (CSS value)","example":"#ffffff"},"font_family":{"type":"string","description":"Default font family injected into the page","example":"Inter, sans-serif"},"font_size":{"type":"string","description":"Default font size injected into the page","example":"14px"},"text_align":{"type":"string","description":"Default text alignment for the page","enum":["left","center","right","justify"],"example":"left"},"horizontal_align":{"type":"string","description":"Horizontal content alignment on the page","enum":["left","center","right"],"example":"left"},"vertical_align":{"type":"string","description":"Vertical content alignment on the page (useful for single-page documents like certificates)","enum":["top","center","bottom"],"example":"top"},"google_fonts_url":{"type":"string","description":"Google Fonts URL or any @import-compatible CSS URL. Loads in <head>, available to template and all components.","example":"https://fonts.googleapis.com/css2?family=Roboto&display=swap"},"color":{"type":"string","description":"Default text color injected into the page","example":"#333333"},"line_height":{"type":"string","description":"Default line-height injected into the page","example":"1.5"},"component_gap":{"type":"string","description":"Gap between header/footer components and page content. Applied as padding between the repeated header/footer and the body content.","example":"5mm"},"footer_position":{"type":"string","enum":["after-content","page-bottom"],"description":"Footer position mode. \"after-content\" (default) places the footer right after the last content row. \"page-bottom\" pins the footer to the bottom of every page, including the last page.","example":"after-content"}}},"ApiError":{"type":"object","description":"Structured error response","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["authentication_error","invalid_request_error","not_found_error","api_error"],"description":"Error category"},"code":{"type":"string","description":"Machine-readable error code","example":"missing_parameter"},"message":{"type":"string","description":"Human-readable error message","example":"Either template_id or html is required"}}}}},"PdfLog":{"type":"object","properties":{"id":{"type":"string"},"template_id":{"type":"string","nullable":true},"template_name":{"type":"string","nullable":true,"description":"Template name at the time of generation (may differ if template was renamed)"},"api_key_id":{"type":"string","nullable":true},"status":{"type":"string","enum":["success","error"]},"duration_ms":{"type":"integer","description":"PDF generation time in milliseconds"},"size_bytes":{"type":"integer","nullable":true,"description":"PDF file size in bytes (null if generation failed)"},"error":{"type":"string","nullable":true,"description":"Error message (null if successful)"},"created_at":{"type":"string","format":"date-time"}}},"CursorPaginatedLogs":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PdfLog"}},"has_more":{"type":"boolean","description":"Whether there are more results after this page"},"next_cursor":{"type":"string","nullable":true,"description":"Cursor to pass as `cursor` query parameter to fetch the next page. Null if no more results."}}},"Stats":{"type":"object","properties":{"templates":{"type":"integer","description":"Total number of templates"},"total_renders":{"type":"integer","description":"Total number of PDF renders"},"success_rate":{"type":"integer","description":"Success rate as percentage (0-100)"},"avg_duration_ms":{"type":"integer","description":"Average render duration in milliseconds"},"total_size_bytes":{"type":"integer","description":"Total size of all generated PDFs in bytes"}}},"ApiKey":{"type":"object","properties":{"id":{"type":"string","description":"API key ID"},"name":{"type":"string","description":"Display name","example":"Production"},"prefix":{"type":"string","description":"Key prefix for identification","example":"p4_live_abc..."},"permission":{"type":"string","enum":["full_access","render_only"],"description":"Permission scope"},"created_at":{"type":"string","format":"date-time"}}}},"responses":{"BadRequest":{"description":"Bad request: missing or invalid parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"type":"invalid_request_error","code":"missing_parameter","message":"Either template_id or html is required"}}}}},"Unauthorized":{"description":"Unauthorized: missing, invalid, or insufficient API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"type":"authentication_error","code":"unauthorized","message":"Unauthorized"}}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"error":{"type":"not_found_error","code":"resource_not_found","message":"Template not found"}}}}}}},"tags":[{"name":"Render","description":"Generate PDFs from templates or raw HTML. This is the core endpoint: most integrations only need this."},{"name":"Templates","description":"CRUD operations for HTML templates. Templates support Handlebars `{{variables}}` and can be referenced by ID or slug in render requests. Requires `full_access` API key scope."},{"name":"Components","description":"Reusable HTML fragments: headers (repeat at top of every page), footers (repeat at bottom), and blocks (render inline). Attach header/footer to templates via header_component_id and footer_component_id. Reference blocks via <pdf4-block> tags in template HTML. Requires `full_access` API key scope."},{"name":"Logs","description":"View PDF generation history. Each render (success or error) is logged with duration, file size, and metadata. Requires `full_access` API key scope."}]}