Documentation
Qorstack Report docs
Design DOCX or XLSX templates, send JSON, get your document. This guide covers every template feature from simple variable replacement to tables, images, QR codes, barcodes, PDF security, and ZIP output.
Quick start
- In a Word file, type placeholders such as
{{customer}}, then upload it on Templates → New template. - Copy the template key (template header) and the project API key (API key page).
- Send the request below. The response contains a download link to the generated PDF.
Everything else — tables, images, QR codes, Excel output, async jobs — is optional and described below.
Overview
Qorstack Report Service generates PDF, DOCX, and Excel documents from Word (.docx) and Excel (.xlsx) templates. Instead of maintaining complex layout code, you design templates visually, upload them, and send a simple JSON payload to generate the final file.
Self-hosted
Template-based generation
Recurring reports, batch generation, and self-host integrations
Use a pre-uploaded DOCX or XLSX template stored in your project. Reference the template by key and send JSON data to render the final document.
/render/word/templateor/render/excel/templateAuth header
X-API-KeyKey field
templateKeyIntegration & SDKs
Choose your preferred integration method. We provide official SDKs for Node.js and .NET, or you can use the REST API directly with any HTTP client.
REST API / Raw HTTP
No installation required. Use any HTTP client.
- API URL
- http://localhost:8080/api/render/word/template
- Header
- Include
X-API-Keyin your request headers.
Request Example
| Property | Type | Required | Plan | Description |
|---|---|---|---|---|
| templateKey | string | Yes | Free | Template identifier for the uploaded DOCX/XLSX template. |
| templateVersion | integer | No | Free | Render an older stored version of the template (see its version history). Omit for the current version; an unknown version returns 404. |
| fileName | string | No | Free | Output PDF filename (default: "output"). |
| replace | KeyValueRequest[] | No | Free | Text variable replacements: [{ "key": "customer_name", "value": "ACME" }]. The v1 object-map form was removed in v2.0 (returns 400). |
| table | array | No | Free | Dynamic table data (TableDataRequest[]): list the column names once in columns, then each row as an array of values in that order, e.g. { "columns": ["item", "qty"], "rows": [["A", 2]] }. |
| image | ImageDataRequest[] | No | Free | Image insertion data, one item per {{image:key}}. |
| qrcode | QrCodeDataRequest[] | No | Free | QR code generation data, one item per key. |
| barcode | BarcodeDataRequest[] | No | Free | Barcode generation data, one item per key. |
| pdfPassword | object | No | Pro | PDF password and permission options. |
| watermark | object | No | Pro | PDF text watermark options. |
| zipOutput | boolean | No | Pro | Return the generated output as a ZIP file. |
| async | boolean | No | Pro | Pro. Queue the render and get 202 + jobId instead of the file. Poll GET /render/jobs/{jobId}. |
| webhookUrl | string | No | Pro | With async: true. Receives a POST { jobId, status, downloadUrl?, error? } when the job finishes. |
Sync vs async & webhooks
There are two ways to call the render API. Sync (the request example above): the call waits and answers 200 with a downloadUrl — best for one document that renders in a few seconds. Async (Pro): add "async": true and the call answers 202 with a jobId at once; the document is built in the background and the result is sent to your webhookUrl. Use it for large documents, batches, or callers that must not wait. Jobs are stored in PostgreSQL, so they survive restarts and several backend instances share the queue; temporary errors (database, network, Gotenberg) are retried up to 3 times.
1. Send the render job
No webhook endpoint? Poll GET /render/jobs/{jobId} (status pending → processing → success | failed); a success answer holds a fresh downloadUrl. The SDKs do this for you with waitForJob / WaitForJobAsync.
2. Receive the webhook in your app
| Property | Type | Required | Plan | Description |
|---|---|---|---|---|
| jobId | string (uuid) | Yes | Free | The id you got in the 202 answer. |
| status | string | Yes | Free | "success" or "failed". |
| downloadUrl | string | No | Free | On success. Works for expiresIn seconds without an API key. |
| expiresIn | integer | No | Free | Seconds the downloadUrl stays valid. |
| error | string | No | Free | On failure: why the render failed. |
Qorstack sends one POST with X-Qorstack-Event: render.completed. Any http(s) address works, internal hosts and private IPs included. Answer 2xx within 10 seconds; other answers are retried after 10 s and 60 s (3 attempts in total), so the same jobId can arrive twice. When the server sets WEBHOOK_SIGNING_SECRET, each call carries X-Qorstack-Signature: sha256=<hex HMAC-SHA256 of the raw body> — check it as below.
Project export & import
Pro. Move a whole project to another deployment, e.g. staging → production: Export & import (project sidebar). Export gives one .zip with the current version of every template, its test data and images, and your uploaded fonts (no API keys, members or history). Import shows a preview first, then creates new templates with the same key, adds a new version when the file, name or test data changed, and leaves identical ones alone — you review every difference first. API: GET /projects/{id}/export and POST /projects/{id}/import?dryRun=true (multipart file).
Variable Replacement
Use {{variable}} in your Word document to define placeholders.
Template (.docx)
Data Payload
| Property | Type | Required | Plan | Description |
|---|---|---|---|---|
| key | string | Yes | Free | Template variable name (e.g. {{name}} -> "name"). |
| value | string | Yes | Free | Replacement text. |
Tables
Create dynamic tables using {{row:field}} markers inside a table row. The API expects a table array where each item contains rows.
Template (.docx)
Put {{row:name}}, {{row:qty}}, and other row markers in the template row that should repeat.
| Property | Type | Required | Plan | Description |
|---|---|---|---|---|
| table | WordTableDataRequest[] | No | Free | Array of table data requests. |
| columns | string[] | Yes | Free | Column names, listed once; each matches a {{row:name}} marker. Must be unique. |
| rows | (string | number | boolean | null)[][] | Yes | Free | Array of rows; each row holds the values in the order of columns. A row with the wrong number of values returns 400 naming the row. |
| repeatHeader | boolean | No | Free | Repeat the Word table header row on each page. |
Tables (Advanced)
Advanced table features including sorting, grouping, vertical merge, collapse, and aggregates.
Advanced Properties
| Property | Type | Required | Plan | Description |
|---|---|---|---|---|
| sort | { field, direction }[] | No | Free | Sorting rules, applied in order (e.g. [{ "field": "price", "direction": "desc" }]). |
| verticalMerge | string[] | No | Free | Merges cells with identical values vertically. Names must be in columns.ex. ["category", "date"] |
| collapse | string[] | No | Free | Collapses identical rows into a single row.ex. ["date", "store_branch"] |
Template Grouping Styles
Header Row Grouping
Place the Header row above the data row to display a group header (used in conjunction with the first verticalMerge item).
| Item Name | Qty | Price |
|---|---|---|
| Category: {{group:category}} ({{group_count}} items) | ||
| {{row:name}} | {{row:qty}} | {{row:price}} |
Note: Use the {{group:field}} marker in the Header Row to indicate where the group name will be displayed.
Footer Grouping
Place a summary row below the data row to show the total at the end of each group.
| Item Name | Qty | Price |
|---|---|---|
| {{row:name}} | {{row:qty}} | {{row:price}} |
| Total for {{group:category}} | {{group_sum:qty}} | Total {{group_sum:price}} |
Aggregates
Table Scope
Used to display the grand totals for the entire table.
{{table_count}}Total item count{{table_sum:field}}Grand total sum{{table_avg:field}}Grand total averageGroup Scope
Used in the Group Header or Group Footer.
{{group_count}}Group item count{{group_sum:field}}Group subtotal sum{{group_avg:field}}Group subtotal averageRow Scope
Used to calculate values between fields in the same row.
{{row_sum:f1,f2}}Sum of specific fields{{row_avg:f1,f2}}Average of specific fieldsImages
Insert images dynamically using {{image:variable}}.
Template (.docx)
Data Payload
| Property | Type | Required | Plan | Description |
|---|---|---|---|---|
| key | string | Yes | Free | Image marker name (e.g. {{image:logo}} -> "logo"). |
| src | string | Yes | Free | Image URL, data URL, or stored file URL. |
| width | number | No | Free | Image width (px). |
| height | number | No | Free | Image height (px). |
| fit | string | No | Free | Image fit: "cover", "contain", "fill". |
QR Codes
Generate QR codes automatically with {{qrcode:variable}}.
Template (.docx)
Data Payload
| Property | Type | Required | Plan | Description |
|---|---|---|---|---|
| key | string | Yes | Free | QR code marker name (e.g. {{qrcode:payment}} -> "payment"). |
| text | string | Yes | Free | Content to encode. |
| size | number | No | Free | QR code size (px). |
| color | string | No | Free | Foreground color, e.g. "#111827". |
| backgroundColor | string | No | Free | Background color. |
| logo | string | No | Free | Optional logo URL placed in the QR code center. |
Barcodes
Generate Barcodes automatically with {{barcode:variable}}.
Template (.docx)
Data Payload
| Property | Type | Required | Plan | Description |
|---|---|---|---|---|
| key | string | Yes | Free | Barcode marker name (e.g. {{barcode:sku}} -> "sku"). |
| text | string | Yes | Free | Content to encode. |
| format | string | No | Free | Barcode format, default "Code128". |
| width | number | No | Free | Barcode width (px). |
| height | number | No | Free | Barcode height (px). |
| includeText | boolean | No | Free | Show human-readable text under the bars. |
| color | string | No | Free | Foreground color. |
| backgroundColor | string | No | Free | Background color. |
File Settings
Configure high-level render options such as PDF password protection, watermarking, and ZIP output.
Template (.docx)
Security & Encryption
Protect your generated PDF files with a password. This uses standard PDF encryption which prevents unauthorized viewing.
| Property | Type | Required | Plan | Description |
|---|---|---|---|---|
| pdfPassword.userPassword | string | No | Pro | Password required to open the generated PDF. |
| pdfPassword.ownerPassword | string | No | Pro | Owner password used to apply PDF permission restrictions. |
| restrictPrinting | boolean | No | Pro | Disallow printing when supported by the PDF reader. |
| restrictCopying | boolean | No | Pro | Disallow copying text or images when supported by the PDF reader. |
| restrictModifying | boolean | No | Pro | Disallow modifying the PDF when supported by the PDF reader. |
Branding & Watermarking
Add a non-removable watermark text across all pages of the document.
| Property | Type | Required | Plan | Description |
|---|---|---|---|---|
| watermark.text | string | No | Pro | Text to be displayed as a watermark (e.g. "CONFIDENTIAL"). |
| fontSize | number | No | Pro | Watermark font size. |
| fontFamily | string | No | Pro | Font family name. |
| fontWeight | number | No | Pro | Font weight, e.g. 400 or 700. |
| fontItalic | boolean | No | Pro | Use italic font style. |
| color | string | No | Pro | Watermark color. |
| opacity | number | No | Pro | Opacity from 0 to 1. |
| rotation | number | No | Pro | Rotation angle in degrees. |
| positionX | string | No | Pro | Horizontal position, e.g. "center". |
| positionY | string | No | Pro | Vertical position, e.g. "center". |
| pages | number[] | No | Pro | Optional 1-based page list. |
Output Packaging
| Property | Type | Required | Plan | Description |
|---|---|---|---|---|
| zipOutput | boolean | No | Pro | Return a ZIP archive instead of the raw generated file. |
Data Payload
Fonts & troubleshooting
PDFs are rendered on the server, so the server needs every font your template uses. Fonts that come with the server are listed as System; add others on the Fonts page (.ttf, .otf, .woff, .woff2). Uploaded fonts are copied to the renderer within about 30 seconds — the Status column shows Syncing until they are Ready.
Thai fonts
Use the exact font your Word file uses — for example TH Sarabun New, Sarabun, or Angsana New. Upload every weight you use (Regular, Bold, Italic, Bold Italic); Word fakes bold when a weight is missing, but the PDF renderer picks the closest real file, so text can look lighter or wider than in Word.
Missing or wrong font
- Text appears in a different font — the font is not on the server. Check the font name in Word (Home → Font), upload that family on the Fonts page, wait for Ready, then generate again.
- Thai vowels or tone marks overlap — the fallback font has no Thai shaping. Upload the Thai font the template uses.
- Status shows Problem — the file could not be read or copied. Delete it and upload the original .ttf/.otf again.
- The preview on the Fonts page looks different — that preview uses fonts installed on your own device. Only the generated PDF shows what the server has.