PDF Templates API Endpoints
REST API for managing PDF templates, components, and rendering.
Overview
The PDF Templates API provides:
- Template management (CRUD, versioning, publishing)
- Component library (reusable blocks)
- Preview & rendering (generate PDFs from templates + data)
Base URL: /v1/pdf-templates
Template Management
GET /v1/pdf-templates
List PDF templates. Org-scoped. Admins see all, specialists see published only.
Query params:
?type=report- Filter by template type?published=true- Filter by published status (admin only)?include=versions- Include version history
Response: 200
{
"data": [
{
"id": 5,
"name": "Standard Consultation Report",
"description": "Default template for consultation reports",
"template_type": "report",
"version": 3,
"published": true,
"components_used": ["letterhead", "signature_block"],
"created_at": "2025-01-10T09:00:00Z",
"updated_at": "2025-02-01T14:30:00Z"
},
{
"id": 6,
"name": "Prescription Template",
"description": "Medication prescription layout",
"template_type": "prescription",
"version": 2,
"published": true,
"components_used": ["letterhead"],
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-20T11:00:00Z"
}
],
"meta": {
"page": 1,
"page_size": 25,
"total": 12,
"total_pages": 1
}
}POST /v1/pdf-templates
Create new PDF template. Admin only.
Request:
{
"name": "Standard Report",
"description": "Default consultation report template",
"template_type": "report",
"template_html": "<!DOCTYPE html><html>...</html>",
"template_css": "body { font-family: Arial; }",
"editor_state": {
"blocks": [
{
"id": "block_1",
"type": "letterhead",
"config": {"showLogo": true}
},
{
"id": "block_2",
"type": "heading",
"config": {"content": "Consultation Report", "level": 2}
}
]
},
"layout_config": {
"pageSize": "A4",
"orientation": "portrait",
"margins": {"top": 20, "right": 20, "bottom": 20, "left": 20}
},
"components_used": ["letterhead", "signature_block"]
}Response: 201
{
"data": {
"id": 5,
"organization_id": 1,
"name": "Standard Report",
"description": "Default consultation report template",
"template_type": "report",
"template_html": "<!DOCTYPE html>...",
"template_css": "body { font-family: Arial; }",
"editor_state": {...},
"layout_config": {...},
"components_used": ["letterhead", "signature_block"],
"version": 1,
"published": false,
"created_at": "2025-02-15T10:00:00Z",
"updated_at": "2025-02-15T10:00:00Z"
}
}Validation:
namemust be unique within organizationtemplate_typemust be valid:report,prescription,disclaimer,certificate,invoicetemplate_htmlmust be valid HTMLlayout_config.pageSizemust be:A4,Letter,Legal,A3,A5
Errors:
400 validation_error- Invalid template data409 duplicate_name- Template name already exists
GET /v1/pdf-templates/{id}
Get template with full details including editor state and version history.
Query params:
?include=versions- Include version history
Response: 200
{
"data": {
"id": 5,
"organization_id": 1,
"name": "Standard Report",
"description": "Default consultation report template",
"template_type": "report",
"template_html": "<!DOCTYPE html>...",
"template_css": "body { font-family: Arial; }",
"editor_state": {
"blocks": [...]
},
"layout_config": {
"pageSize": "A4",
"orientation": "portrait",
"margins": {"top": 20, "right": 20, "bottom": 20, "left": 20}
},
"components_used": ["letterhead", "signature_block"],
"version": 3,
"published": true,
"created_by": 10,
"updated_by": 10,
"created_at": "2025-01-10T09:00:00Z",
"updated_at": "2025-02-01T14:30:00Z",
"versions": [
{
"version": 3,
"published_at": "2025-02-01T14:30:00Z",
"changed_by": 10,
"change_notes": "Updated footer layout"
},
{
"version": 2,
"published_at": "2025-01-20T11:00:00Z",
"changed_by": 10,
"change_notes": "Added signature block"
},
{
"version": 1,
"published_at": "2025-01-10T09:00:00Z",
"changed_by": 10,
"change_notes": null
}
]
}
}PUT /v1/pdf-templates/{id}
Update template (saves as draft). Admin only.
Request:
{
"description": "Updated description",
"template_html": "<!DOCTYPE html>...",
"template_css": "body { font-family: Helvetica; }",
"editor_state": {
"blocks": [...]
},
"layout_config": {
"pageSize": "A4",
"orientation": "landscape"
}
}Response: 200
{
"data": {
"id": 5,
"name": "Standard Report",
"version": 3,
"published": false,
"updated_at": "2025-02-15T11:00:00Z"
}
}Business logic:
- Validate template is not in use by active documents (or allow with warning)
- Set
published = false(draft mode) - Update template fields
- Set
updated_byto current user - Version number remains unchanged until publish
Errors:
404 template_not_found- Template doesn't exist403 forbidden- Not admin400 validation_error- Invalid template data
POST /v1/pdf-templates/{id}/publish
Publish template (increment version, archive previous). Admin only.
Request:
{
"change_notes": "Updated footer layout"
}Response: 200
{
"data": {
"id": 5,
"version": 4,
"published": true,
"published_at": "2025-02-15T11:30:00Z"
}
}Business logic:
- Validate template exists and is in draft mode
- Archive current version to
pdf_template_versions - Increment
version(3 → 4) - Set
published = true - Record
changed_byandchange_notes
Errors:
404 template_not_found403 forbidden- Not admin400 already_published- Template is already published
POST /v1/pdf-templates/{id}/rollback/{version}
Rollback to a previous version. Admin only.
Request: (empty body)
Response: 200
{
"data": {
"id": 5,
"version": 2,
"published": false,
"updated_at": "2025-02-15T12:00:00Z"
}
}Business logic:
- Fetch version from
pdf_template_versions - Copy
template_html,template_css,editor_state,layout_configtopdf_templates - Set
versionto specified version - Set
published = false(requires re-publish)
Errors:
404 version_not_found- Version doesn't exist403 forbidden- Not admin
DELETE /v1/pdf-templates/{id}
Delete template. Admin only. Cannot delete if referenced by documents.
Response: 200
{
"data": {
"id": 5,
"message": "Template deleted successfully"
}
}Business logic:
- Check if template is referenced by any
form_templatesorappointment_documents - If referenced, return 409 error
- Otherwise, cascade delete (versions deleted automatically via FK CASCADE)
Errors:
404 template_not_found403 forbidden- Not admin409 template_in_use- Cannot delete, template is referenced
Component Library
GET /v1/pdf-template-components
List reusable components. Org-scoped.
Query params:
?category=letterhead- Filter by category
Response: 200
{
"data": [
{
"id": 10,
"name": "Standard Letterhead",
"description": "Organization header with logo",
"category": "letterhead",
"component_html": "<div class=\"letterhead\">...</div>",
"component_css": ".letterhead { text-align: center; }",
"thumbnail_url": "https://s3.../thumbnails/letterhead.png",
"variables_used": ["Organization.LogoURL", "Organization.Name"],
"created_at": "2025-01-10T09:00:00Z"
},
{
"id": 11,
"name": "Signature Block",
"description": "Specialist signature with license",
"category": "signature",
"component_html": "<div class=\"signature\">...</div>",
"component_css": ".signature { margin-top: 20mm; }",
"thumbnail_url": "https://s3.../thumbnails/signature.png",
"variables_used": ["Specialist.Name", "Specialist.SignatureURL"],
"created_at": "2025-01-10T09:00:00Z"
}
]
}POST /v1/pdf-template-components
Create reusable component. Admin only.
Request:
{
"name": "Custom Footer",
"description": "Footer with page numbers",
"category": "footer",
"component_html": "<div class=\"footer\">Page {{.PageNumber}}</div>",
"component_css": ".footer { text-align: center; font-size: 10pt; }",
"variables_used": ["PageNumber"]
}Response: 201
{
"data": {
"id": 12,
"organization_id": 1,
"name": "Custom Footer",
"description": "Footer with page numbers",
"category": "footer",
"component_html": "<div class=\"footer\">...</div>",
"component_css": ".footer { text-align: center; }",
"variables_used": ["PageNumber"],
"created_at": "2025-02-15T10:00:00Z"
}
}Categories: header, footer, signature, letterhead, table, section
PUT /v1/pdf-template-components/{id}
Update component. Admin only.
Request:
{
"description": "Updated description",
"component_html": "<div class=\"footer\">...</div>",
"component_css": ".footer { ... }"
}Response: 200
{
"data": {
"id": 12,
"name": "Custom Footer",
"updated_at": "2025-02-15T11:00:00Z"
}
}DELETE /v1/pdf-template-components/{id}
Delete component. Admin only. Warns if component is in use.
Response: 200
{
"data": {
"id": 12,
"message": "Component deleted successfully"
}
}Errors:
409 component_in_use- Component is referenced by templates (warning, not blocking)
Preview & Rendering
GET /v1/pdf-templates/{id}/preview
Preview template with sample data. Returns PDF or HTML.
Query params:
?format=html- Return HTML preview (default: PDF)?sample_data=true- Use sample form data
Response: 200 (PDF)
- Content-Type:
application/pdf - Binary PDF data
Response: 200 (HTML)
{
"data": {
"html": "<!DOCTYPE html>...",
"css": "body { font-family: Arial; }"
}
}Business logic:
- Load template from database
- Use sample data for template variables:json
{ "Organization": {"name": "Sample Clinic", "address": "123 Main St"}, "Patient": {"name": "John Doe", "birthdate": "1985-05-15"}, "Specialist": {"name": "Dr. Smith", "title": "MD"}, "FormValues": {"field_10": "Sample value"} } - Merge template + data using Go templates
- Render HTML → PDF with chromedp (if format=pdf)
POST /v1/pdf-templates/{id}/render
Render template with real form data. Returns PDF.
Request:
{
"form_id": 201,
"document_type": "report"
}Response: 201
{
"data": {
"document_id": 456,
"file_url": "https://s3.amazonaws.com/bucket/org-1/documents/report_456.pdf",
"template_version": 3,
"generated_at": "2025-02-15T12:00:00Z"
}
}Business logic:
- Validate form exists and is signed
- Load form data (values, fields)
- Load patient, specialist, appointment, organization
- Merge template + data using Go templates
- Render HTML → PDF with chromedp
- Upload PDF to S3
- Create
appointment_documentsrecord with template version
Errors:
404 form_not_found400 form_not_signed- Form must be signed before PDF generation404 template_not_found500 rendering_error- PDF generation failed
Batch Operations
POST /v1/pdf-templates/batch-render
Render multiple PDFs from multiple forms (batch generation).
Request:
{
"template_id": 5,
"form_ids": [201, 202, 203]
}Response: 202 Accepted
{
"data": {
"job_id": "batch_123",
"status": "processing",
"total": 3,
"completed": 0
}
}Poll status:
GET /v1/pdf-templates/batch-render/{job_id}Response: 200
{
"data": {
"job_id": "batch_123",
"status": "completed",
"total": 3,
"completed": 3,
"documents": [
{"document_id": 456, "file_url": "..."},
{"document_id": 457, "file_url": "..."},
{"document_id": 458, "file_url": "..."}
]
}
}Error Codes
| Code | HTTP | Description |
|---|---|---|
template_not_found | 404 | Template doesn't exist or not accessible |
version_not_found | 404 | Template version doesn't exist |
component_not_found | 404 | Component doesn't exist |
duplicate_name | 409 | Template/component name already exists |
validation_error | 400 | Invalid template data |
already_published | 400 | Template is already published |
template_in_use | 409 | Cannot delete, template is referenced |
component_in_use | 409 | Component is referenced by templates |
form_not_signed | 400 | Form must be signed before PDF generation |
rendering_error | 500 | PDF generation failed |
forbidden | 403 | Insufficient permissions |
Access Control Summary
| Endpoint | Patient | Specialist | Admin |
|---|---|---|---|
GET /v1/pdf-templates | ❌ | ✅ (published only) | ✅ (all) |
POST /v1/pdf-templates | ❌ | ❌ | ✅ |
PUT /v1/pdf-templates/{id} | ❌ | ❌ | ✅ |
POST /v1/pdf-templates/{id}/publish | ❌ | ❌ | ✅ |
DELETE /v1/pdf-templates/{id} | ❌ | ❌ | ✅ |
GET /v1/pdf-template-components | ❌ | ✅ | ✅ |
POST /v1/pdf-template-components | ❌ | ❌ | ✅ |
GET /v1/pdf-templates/{id}/preview | ❌ | ✅ | ✅ |
POST /v1/pdf-templates/{id}/render | ❌ | ✅ | ✅ |
Go Service Interface
type PDFTemplateService interface {
// Template CRUD
ListTemplates(ctx context.Context, filter TemplateFilter) ([]PDFTemplate, error)
GetTemplate(ctx context.Context, templateID int64) (*PDFTemplate, error)
CreateTemplate(ctx context.Context, input CreateTemplateInput) (*PDFTemplate, error)
UpdateTemplate(ctx context.Context, templateID int64, input UpdateTemplateInput) (*PDFTemplate, error)
DeleteTemplate(ctx context.Context, templateID int64) error
// Versioning
PublishTemplate(ctx context.Context, templateID int64, notes string) (*PDFTemplate, error)
GetVersionHistory(ctx context.Context, templateID int64) ([]PDFTemplateVersion, error)
RollbackToVersion(ctx context.Context, templateID int64, version int) (*PDFTemplate, error)
// Component library
ListComponents(ctx context.Context, category string) ([]PDFTemplateComponent, error)
CreateComponent(ctx context.Context, input CreateComponentInput) (*PDFTemplateComponent, error)
UpdateComponent(ctx context.Context, componentID int64, input UpdateComponentInput) (*PDFTemplateComponent, error)
DeleteComponent(ctx context.Context, componentID int64) error
// Rendering
PreviewTemplate(ctx context.Context, templateID int64, format string) ([]byte, error)
RenderPDF(ctx context.Context, templateID int64, formID int64) (*Document, error)
BatchRenderPDF(ctx context.Context, templateID int64, formIDs []int64) (string, error) // returns job_id
}