Skip to content

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

json
{
  "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:

json
{
  "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

json
{
  "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:

  • name must be unique within organization
  • template_type must be valid: report, prescription, disclaimer, certificate, invoice
  • template_html must be valid HTML
  • layout_config.pageSize must be: A4, Letter, Legal, A3, A5

Errors:

  • 400 validation_error - Invalid template data
  • 409 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

json
{
  "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:

json
{
  "description": "Updated description",
  "template_html": "<!DOCTYPE html>...",
  "template_css": "body { font-family: Helvetica; }",
  "editor_state": {
    "blocks": [...]
  },
  "layout_config": {
    "pageSize": "A4",
    "orientation": "landscape"
  }
}

Response: 200

json
{
  "data": {
    "id": 5,
    "name": "Standard Report",
    "version": 3,
    "published": false,
    "updated_at": "2025-02-15T11:00:00Z"
  }
}

Business logic:

  1. Validate template is not in use by active documents (or allow with warning)
  2. Set published = false (draft mode)
  3. Update template fields
  4. Set updated_by to current user
  5. Version number remains unchanged until publish

Errors:

  • 404 template_not_found - Template doesn't exist
  • 403 forbidden - Not admin
  • 400 validation_error - Invalid template data

POST /v1/pdf-templates/{id}/publish

Publish template (increment version, archive previous). Admin only.

Request:

json
{
  "change_notes": "Updated footer layout"
}

Response: 200

json
{
  "data": {
    "id": 5,
    "version": 4,
    "published": true,
    "published_at": "2025-02-15T11:30:00Z"
  }
}

Business logic:

  1. Validate template exists and is in draft mode
  2. Archive current version to pdf_template_versions
  3. Increment version (3 → 4)
  4. Set published = true
  5. Record changed_by and change_notes

Errors:

  • 404 template_not_found
  • 403 forbidden - Not admin
  • 400 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

json
{
  "data": {
    "id": 5,
    "version": 2,
    "published": false,
    "updated_at": "2025-02-15T12:00:00Z"
  }
}

Business logic:

  1. Fetch version from pdf_template_versions
  2. Copy template_html, template_css, editor_state, layout_config to pdf_templates
  3. Set version to specified version
  4. Set published = false (requires re-publish)

Errors:

  • 404 version_not_found - Version doesn't exist
  • 403 forbidden - Not admin

DELETE /v1/pdf-templates/{id}

Delete template. Admin only. Cannot delete if referenced by documents.

Response: 200

json
{
  "data": {
    "id": 5,
    "message": "Template deleted successfully"
  }
}

Business logic:

  1. Check if template is referenced by any form_templates or appointment_documents
  2. If referenced, return 409 error
  3. Otherwise, cascade delete (versions deleted automatically via FK CASCADE)

Errors:

  • 404 template_not_found
  • 403 forbidden - Not admin
  • 409 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

json
{
  "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:

json
{
  "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

json
{
  "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:

json
{
  "description": "Updated description",
  "component_html": "<div class=\"footer\">...</div>",
  "component_css": ".footer { ... }"
}

Response: 200

json
{
  "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

json
{
  "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)

json
{
  "data": {
    "html": "<!DOCTYPE html>...",
    "css": "body { font-family: Arial; }"
  }
}

Business logic:

  1. Load template from database
  2. 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"}
    }
  3. Merge template + data using Go templates
  4. Render HTML → PDF with chromedp (if format=pdf)

POST /v1/pdf-templates/{id}/render

Render template with real form data. Returns PDF.

Request:

json
{
  "form_id": 201,
  "document_type": "report"
}

Response: 201

json
{
  "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:

  1. Validate form exists and is signed
  2. Load form data (values, fields)
  3. Load patient, specialist, appointment, organization
  4. Merge template + data using Go templates
  5. Render HTML → PDF with chromedp
  6. Upload PDF to S3
  7. Create appointment_documents record with template version

Errors:

  • 404 form_not_found
  • 400 form_not_signed - Form must be signed before PDF generation
  • 404 template_not_found
  • 500 rendering_error - PDF generation failed

Batch Operations

POST /v1/pdf-templates/batch-render

Render multiple PDFs from multiple forms (batch generation).

Request:

json
{
  "template_id": 5,
  "form_ids": [201, 202, 203]
}

Response: 202 Accepted

json
{
  "data": {
    "job_id": "batch_123",
    "status": "processing",
    "total": 3,
    "completed": 0
  }
}

Poll status:

GET /v1/pdf-templates/batch-render/{job_id}

Response: 200

json
{
  "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

CodeHTTPDescription
template_not_found404Template doesn't exist or not accessible
version_not_found404Template version doesn't exist
component_not_found404Component doesn't exist
duplicate_name409Template/component name already exists
validation_error400Invalid template data
already_published400Template is already published
template_in_use409Cannot delete, template is referenced
component_in_use409Component is referenced by templates
form_not_signed400Form must be signed before PDF generation
rendering_error500PDF generation failed
forbidden403Insufficient permissions

Access Control Summary

EndpointPatientSpecialistAdmin
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

go
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
}