QORSTACKREPORT
How it worksFeaturesDevelopersPricingDocs
Star—Get started

Getting Started

Template Features

Help

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.

01 — Quick start

Quick start

  1. In a Word file, type placeholders such as {{customer}}, then upload it on Templates → New template.
  2. Copy the template key (template header) and the project API key (API key page).
  3. Send the request below. The response contains a download link to the generated PDF.
Request
1curl -X POST http://localhost:8080/api/render/word/template \
2  -H "X-API-Key: YOUR_API_KEY" \
3  -H "Content-Type: application/json" \
4  -d '{
5    "templateKey": "YOUR_TEMPLATE_KEY",
6    "replace": [{ "key": "customer", "value": "Siam Tech Co., Ltd." }]
7  }'
Response
1{
2  "jobId": "6f1c2a8e-…",
3  "downloadUrl": "http://localhost:8080/api/files/eyJ…",
4  "expiresIn": 3600,
5  "status": "success",
6  "fileType": "pdf"
7}

Everything else — tables, images, QR codes, Excel output, async jobs — is optional and described below.

02 — Overview

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

Qorstack Report runs on your own servers: templates, JSON payloads and generated files stay in your PostgreSQL and S3 storage and are never sent to Qorstack. Temporary downloads are deleted after one day.

Template-based generation

Recurring reports, batch generation, and self-host integrations

POST

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.

POST/render/word/templateor/render/excel/template

Auth header

X-API-Key

Key field

templateKey
03 — Integration & SDKs

Integration & 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-Key in your request headers.

Request Example

PropertyTypeRequiredPlanDescription
templateKeystringYesFreeTemplate identifier for the uploaded DOCX/XLSX template.
templateVersionintegerNoFreeRender an older stored version of the template (see its version history). Omit for the current version; an unknown version returns 404.
fileNamestringNoFreeOutput PDF filename (default: "output").
replaceKeyValueRequest[]NoFreeText variable replacements: [{ "key": "customer_name", "value": "ACME" }]. The v1 object-map form was removed in v2.0 (returns 400).
tablearrayNoFreeDynamic 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]] }.
imageImageDataRequest[]NoFreeImage insertion data, one item per {{image:key}}.
qrcodeQrCodeDataRequest[]NoFreeQR code generation data, one item per key.
barcodeBarcodeDataRequest[]NoFreeBarcode generation data, one item per key.
pdfPasswordobjectNoProPDF password and permission options.
watermarkobjectNoProPDF text watermark options.
zipOutputbooleanNoProReturn the generated output as a ZIP file.
asyncbooleanNoProPro. Queue the render and get 202 + jobId instead of the file. Poll GET /render/jobs/{jobId}.
webhookUrlstringNoProWith async: true. Receives a POST { jobId, status, downloadUrl?, error? } when the job finishes.
1# Create an API key on the API key page, then:
2export QORSTACK_API_KEY="your-api-key"
3
4# The call waits until the file is ready.
5curl -X POST "http://localhost:8080/api/render/word/template" \
6  -H "Content-Type: application/json" \
7  -H "X-API-Key: $QORSTACK_API_KEY" \
8  --data-binary @- <<'JSON'
9{
10  "templateKey": "YOUR_TEMPLATE_KEY",
11  "fileType": "pdf",
12  "fileName": "sales_report_2026",
13  "pdfPassword": {
14    "userPassword": "viewer-pass",
15    "ownerPassword": "owner-pass",
16    "restrictPrinting": true,
17    "restrictCopying": true,
18    "restrictModifying": true
19  },
20  "watermark": {
21    "text": "CONFIDENTIAL",
22    "fontSize": 48,
23    "fontFamily": "Helvetica",
24    "fontWeight": 700,
25    "fontItalic": false,
26    "color": "#64748B",
27    "opacity": 0.14,
28    "rotation": -45,
29    "positionX": "center",
30    "positionY": "center",
31    "pages": [
32      1
33    ]
34  },
35  "zipOutput": false,
36  "replace": [
37    {
38      "key": "report_date",
39      "value": "2026-05-20"
40    },
41    {
42      "key": "prepared_by",
43      "value": "Operations Team"
44    }
45  ],
46  "table": [
47    {
48      "columns": [
49        "item",
50        "qty",
51        "price"
52      ],
53      "rows": [
54        [
55          "Product A",
56          "10",
57          "100"
58        ],
59        [
60          "Product B",
61          "5",
62          "200"
63        ]
64      ]
65    }
66  ],
67  "image": [
68    {
69      "key": "header_logo",
70      "src": "https://example.com/logo.png",
71      "width": 150,
72      "height": 50,
73      "fit": "contain"
74    }
75  ],
76  "qrcode": [
77    {
78      "key": "scan_me",
79      "text": "https://qorstack.dev",
80      "size": 100
81    }
82  ],
83  "barcode": [
84    {
85      "key": "order_id",
86      "text": "ORD-2026-001",
87      "format": "Code128",
88      "width": 200,
89      "height": 50,
90      "includeText": true,
91      "color": "#111827",
92      "backgroundColor": "#FFFFFF"
93    }
94  ]
95}
96JSON
97
98# Answer: 200 OK
99# { "jobId": "6f1c2a8e-…", "downloadUrl": "https://…", "expiresIn": 3600, "status": "success", "fileType": "pdf" }
100
101# Download the file (the link works for expiresIn seconds and needs no API key):
102curl -L -o output.pdf "DOWNLOAD_URL"

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

1# Create an API key on the API key page, then:
2export QORSTACK_API_KEY="your-api-key"
3
4# 1) Queue the render. The call answers at once with a job id (Pro feature).
5curl -X POST "http://localhost:8080/api/render/word/template" \
6  -H "Content-Type: application/json" \
7  -H "X-API-Key: $QORSTACK_API_KEY" \
8  --data-binary @- <<'JSON'
9{
10  "templateKey": "YOUR_TEMPLATE_KEY",
11  "fileType": "pdf",
12  "fileName": "invoice-2026-001",
13  "zipOutput": false,
14  "replace": [
15    {
16      "key": "customer_name",
17      "value": "Acme Co., Ltd."
18    },
19    {
20      "key": "invoice_number",
21      "value": "INV-2026-001"
22    }
23  ],
24  "table": [],
25  "image": [],
26  "qrcode": [],
27  "barcode": [],
28  "async": true,
29  "webhookUrl": "https://your-app.example.com/webhooks/qorstack"
30}
31JSON
32
33# Answer: 202 Accepted
34# { "jobId": "6f1c2a8e-…", "status": "pending", "statusUrl": "/render/jobs/6f1c2a8e-…" }
35
36# 2) When the job finishes, Qorstack POSTs JSON to webhookUrl:
37#    { "jobId": "6f1c2a8e-…", "status": "success", "downloadUrl": "https://…", "expiresIn": 3600 }
38#    { "jobId": "6f1c2a8e-…", "status": "failed", "error": "…" }
39#    Headers: X-Qorstack-Event: render.completed
40#             X-Qorstack-Signature: sha256=<HMAC>   (only when the server sets WEBHOOK_SIGNING_SECRET)
41#    Answer 2xx within 10 s; otherwise it is retried after 10 s and 60 s (3 attempts in total).
42
43# 3) Optional: check the job yourself (pending → processing → success | failed).
44#    Once the status is "success" the answer has a fresh downloadUrl.
45curl "http://localhost:8080/api/render/jobs/JOB_ID" -H "X-API-Key: $QORSTACK_API_KEY"
202 Accepted
1{
2  "jobId": "6f1c2a8e-…",
3  "status": "pending",
4  "statusUrl": "/render/jobs/6f1c2a8e-…"
5}

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

PropertyTypeRequiredPlanDescription
jobIdstring (uuid)YesFreeThe id you got in the 202 answer.
statusstringYesFree"success" or "failed".
downloadUrlstringNoFreeOn success. Works for expiresIn seconds without an API key.
expiresInintegerNoFreeSeconds the downloadUrl stays valid.
errorstringNoFreeOn 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.

1# Qorstack sends one POST to your webhookUrl when an async job finishes.
2#
3# POST /webhooks/qorstack
4# Content-Type: application/json
5# User-Agent: QorstackReport-Webhook/2
6# X-Qorstack-Event: render.completed
7# X-Qorstack-Signature: sha256=<hex>   (only when the server sets WEBHOOK_SIGNING_SECRET)
8#
9# Success: { "jobId": "6f1c2a8e-…", "status": "success", "downloadUrl": "https://…", "expiresIn": 3600 }
10# Failure: { "jobId": "6f1c2a8e-…", "status": "failed", "error": "Template not found" }
11#
12# Rules for your endpoint:
13#  - Answer any 2xx within 10 seconds. Anything else is retried after 10 s and 60 s (3 attempts in total),
14#    so the same jobId can arrive more than once — handle it idempotently.
15#  - downloadUrl works for expiresIn seconds and needs no API key. Download it, or later ask
16#    GET /render/jobs/{jobId} (with your API key) for a fresh link.
17#  - Signature: hex HMAC-SHA256 of the raw request body with the shared secret, prefixed "sha256=".
18#    Compute it over the exact bytes received (before any JSON parsing) and compare in constant time.
19
20# Try your endpoint with a signed test call (bash + openssl):
21SECRET="same value as WEBHOOK_SIGNING_SECRET on the Qorstack server"
22BODY='{"jobId":"6f1c2a8e-0000-0000-0000-000000000000","status":"success","downloadUrl":"https://example.com/file.pdf","expiresIn":3600}'
23SIG="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')"
24
25curl -X POST "https://your-app.example.com/webhooks/qorstack" \
26  -H "Content-Type: application/json" \
27  -H "X-Qorstack-Event: render.completed" \
28  -H "X-Qorstack-Signature: $SIG" \
29  --data-raw "$BODY"

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).

04 — Variable Replacement

Variable Replacement

Use {{variable}} in your Word document to define placeholders.

Template (.docx)

Variables in Word

Data Payload

PropertyTypeRequiredPlanDescription
keystringYesFreeTemplate variable name (e.g. {{name}} -> "name").
valuestringYesFreeReplacement text.
1{
2  "templateKey": "YOUR_TEMPLATE_KEY",
3  "fileType": "pdf",
4  "fileName": "invoice-2026-001",
5  "zipOutput": false,
6  "replace": [
7    {
8      "key": "customer_name",
9      "value": "Acme Co., Ltd."
10    },
11    {
12      "key": "invoice_number",
13      "value": "INV-2026-001"
14    },
15    {
16      "key": "issue_date",
17      "value": "2026-05-20"
18    },
19    {
20      "key": "due_date",
21      "value": "2026-06-20"
22    },
23    {
24      "key": "subtotal",
25      "value": "7200"
26    },
27    {
28      "key": "tax",
29      "value": "504"
30    },
31    {
32      "key": "total",
33      "value": "7704"
34    },
35    {
36      "key": "notes",
37      "value": "Generated from the current render API."
38    }
39  ],
40  "table": [],
41  "image": [],
42  "qrcode": [],
43  "barcode": []
44}
05 — Tables

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.

Table in Word
PropertyTypeRequiredPlanDescription
tableWordTableDataRequest[]NoFreeArray of table data requests.
columnsstring[]YesFreeColumn names, listed once; each matches a {{row:name}} marker. Must be unique.
rows(string | number | boolean | null)[][]YesFreeArray 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.
repeatHeaderbooleanNoFreeRepeat the Word table header row on each page.
1{
2  "templateKey": "YOUR_TEMPLATE_KEY",
3  "fileType": "pdf",
4  "fileName": "product-catalog",
5  "zipOutput": false,
6  "replace": [],
7  "table": [
8    {
9      "columns": [
10        "category",
11        "name",
12        "stock",
13        "price"
14      ],
15      "rows": [
16        [
17          "Electronics",
18          "Laptop Pro",
19          "15",
20          "1200"
21        ],
22        [
23          "Electronics",
24          "Wireless Mouse",
25          "50",
26          "25"
27        ],
28        [
29          "Furniture",
30          "Ergo Chair",
31          "8",
32          "350"
33        ],
34        [
35          "Furniture",
36          "Standing Desk",
37          "12",
38          "500"
39        ]
40      ],
41      "sort": [
42        {
43          "field": "category",
44          "direction": "asc"
45        },
46        {
47          "field": "price",
48          "direction": "desc"
49        }
50      ],
51      "verticalMerge": [
52        "category"
53      ],
54      "collapse": [
55        "category"
56      ]
57    }
58  ],
59  "image": [],
60  "qrcode": [],
61  "barcode": []
62}
06 — Tables (Advanced)

Tables (Advanced)

Advanced table features including sorting, grouping, vertical merge, collapse, and aggregates.

Advanced Properties

PropertyTypeRequiredPlanDescription
sort{ field, direction }[]NoFreeSorting rules, applied in order (e.g. [{ "field": "price", "direction": "desc" }]).
verticalMergestring[]NoFree
Merges cells with identical values vertically. Names must be in columns.ex. ["category", "date"]
collapsestring[]NoFree
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 NameQtyPrice
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 NameQtyPrice
{{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 average
Group 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 average
Row 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 fields
07 — Images

Images

Insert images dynamically using {{image:variable}}.

Template (.docx)

Images in Word

Data Payload

PropertyTypeRequiredPlanDescription
keystringYesFreeImage marker name (e.g. {{image:logo}} -> "logo").
srcstringYesFreeImage URL, data URL, or stored file URL.
widthnumberNoFreeImage width (px).
heightnumberNoFreeImage height (px).
fitstringNoFreeImage fit: "cover", "contain", "fill".
1{
2  "templateKey": "YOUR_TEMPLATE_KEY",
3  "fileType": "pdf",
4  "fileName": "company-profile",
5  "zipOutput": false,
6  "replace": [],
7  "table": [],
8  "image": [
9    {
10      "key": "company_logo",
11      "src": "https://example.com/logo.png",
12      "width": 150,
13      "height": 50,
14      "fit": "contain"
15    },
16    {
17      "key": "signature",
18      "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
19      "width": 200,
20      "height": 100,
21      "fit": "contain"
22    }
23  ],
24  "qrcode": [],
25  "barcode": []
26}
08 — QR Codes

QR Codes

Generate QR codes automatically with {{qrcode:variable}}.

Template (.docx)

QR Code in Word

Data Payload

PropertyTypeRequiredPlanDescription
keystringYesFreeQR code marker name (e.g. {{qrcode:payment}} -> "payment").
textstringYesFreeContent to encode.
sizenumberNoFreeQR code size (px).
colorstringNoFreeForeground color, e.g. "#111827".
backgroundColorstringNoFreeBackground color.
logostringNoFreeOptional logo URL placed in the QR code center.
1{
2  "templateKey": "YOUR_TEMPLATE_KEY",
3  "fileType": "pdf",
4  "fileName": "ticket-001",
5  "zipOutput": false,
6  "replace": [],
7  "table": [],
8  "image": [],
9  "qrcode": [
10    {
11      "key": "ticket_qr",
12      "text": "https://event.com/ticket/123456",
13      "size": 200
14    },
15    {
16      "key": "wifi_access",
17      "text": "WIFI:S:MyNetwork;T:WPA;P:password123;;",
18      "size": 150
19    }
20  ],
21  "barcode": []
22}
09 — Barcodes

Barcodes

Generate Barcodes automatically with {{barcode:variable}}.

Template (.docx)

Barcode in Word

Data Payload

PropertyTypeRequiredPlanDescription
keystringYesFreeBarcode marker name (e.g. {{barcode:sku}} -> "sku").
textstringYesFreeContent to encode.
formatstringNoFreeBarcode format, default "Code128".
widthnumberNoFreeBarcode width (px).
heightnumberNoFreeBarcode height (px).
includeTextbooleanNoFreeShow human-readable text under the bars.
colorstringNoFreeForeground color.
backgroundColorstringNoFreeBackground color.
1{
2  "templateKey": "YOUR_TEMPLATE_KEY",
3  "fileType": "pdf",
4  "fileName": "inventory-label",
5  "zipOutput": false,
6  "replace": [],
7  "table": [],
8  "image": [],
9  "qrcode": [],
10  "barcode": [
11    {
12      "key": "sku_barcode",
13      "text": "SKU-99887766",
14      "format": "Code128",
15      "width": 200,
16      "height": 60,
17      "includeText": true,
18      "color": "#111827",
19      "backgroundColor": "#FFFFFF"
20    },
21    {
22      "key": "serial_number",
23      "text": "SN-2026-001-X",
24      "format": "Code128",
25      "width": 300,
26      "height": 80,
27      "includeText": true,
28      "color": "#111827",
29      "backgroundColor": "#FFFFFF"
30    }
31  ]
32}
10 — File Settings

File Settings

Configure high-level render options such as PDF password protection, watermarking, and ZIP output.

Template (.docx)

PDF password watermark and ZIP output

Security & Encryption

Protect your generated PDF files with a password. This uses standard PDF encryption which prevents unauthorized viewing.

PropertyTypeRequiredPlanDescription
pdfPassword.userPasswordstringNoProPassword required to open the generated PDF.
pdfPassword.ownerPasswordstringNoProOwner password used to apply PDF permission restrictions.
restrictPrintingbooleanNoProDisallow printing when supported by the PDF reader.
restrictCopyingbooleanNoProDisallow copying text or images when supported by the PDF reader.
restrictModifyingbooleanNoProDisallow modifying the PDF when supported by the PDF reader.

Branding & Watermarking

Add a non-removable watermark text across all pages of the document.

PropertyTypeRequiredPlanDescription
watermark.textstringNoProText to be displayed as a watermark (e.g. "CONFIDENTIAL").
fontSizenumberNoProWatermark font size.
fontFamilystringNoProFont family name.
fontWeightnumberNoProFont weight, e.g. 400 or 700.
fontItalicbooleanNoProUse italic font style.
colorstringNoProWatermark color.
opacitynumberNoProOpacity from 0 to 1.
rotationnumberNoProRotation angle in degrees.
positionXstringNoProHorizontal position, e.g. "center".
positionYstringNoProVertical position, e.g. "center".
pagesnumber[]NoProOptional 1-based page list.

Output Packaging

PropertyTypeRequiredPlanDescription
zipOutputbooleanNoProReturn a ZIP archive instead of the raw generated file.

Data Payload

1{
2  "templateKey": "YOUR_TEMPLATE_KEY",
3  "fileType": "pdf",
4  "fileName": "protected_contract",
5  "pdfPassword": {
6    "userPassword": "viewer-pass",
7    "ownerPassword": "owner-pass",
8    "restrictPrinting": true,
9    "restrictCopying": true,
10    "restrictModifying": true
11  },
12  "watermark": {
13    "text": "CONFIDENTIAL",
14    "fontSize": 48,
15    "fontFamily": "Helvetica",
16    "fontWeight": 700,
17    "fontItalic": false,
18    "color": "#64748B",
19    "opacity": 0.14,
20    "rotation": -45,
21    "positionX": "center",
22    "positionY": "center",
23    "pages": [
24      1,
25      2
26    ]
27  },
28  "zipOutput": true,
29  "replace": [
30    {
31      "key": "customer_name",
32      "value": "Acme Co., Ltd."
33    },
34    {
35      "key": "contract_date",
36      "value": "2026-05-20"
37    }
38  ],
39  "table": [],
40  "image": [],
41  "qrcode": [],
42  "barcode": []
43}
11 — Fonts & troubleshooting

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.
NextFree vs Pro

On this page

QORSTACKREPORT · MIT CORE
DocsSelf-hostGitHubPricing

© 2026 Qorstack Report. All rights reserved.

Templates in · PDF & Excel out