Get started
Your first request takes three steps.
Get credentials
Generate an API key and secret under API Settings in your Qpro+ account. The secret is shown once.
Name the template and fields
Design a template in the canvas and map its API fields. The template name and field names are your contract.
Call Fetch Markups
Send the name and your values. Check the response, then switch the path to the output you need.
curl -X POST "https://api.beta.quandosol.com/api/custom-labels/fetch-markups" \
-H "Content-Type: application/json" \
-H "X-API-KEY: $QPRO_API_KEY" \
-H "X-API-SECRET: $QPRO_API_SECRET" \
-d '{
"label_name": "YOUR-TEMPLATE",
"amount": 1,
"apiData": { "your_field": "your value" }
}'
Start with Fetch Markups. If the values in its response are right, every other method renders them the same way, and you've separated “is my data right” from “is my printer set up”.
Get started
Every example here runs against the sandbox.
The sandbox base URL below is the one used throughout this reference. Every route sits under /custom-labels/.
| Environment | Base URL | Use it for |
|---|---|---|
| Sandbox | https://api.beta.quandosol.com/api | Qpro+ sandbox accounts: evaluation and development |
Production behaves identically. Your production base URL arrives with your production credentials, and moving over means changing the base URL and the key pair, nothing else.
beta.quandosol.com serves the web app, and a request sent there can come back 200 with an HTML page. If the response Content-Type isn't JSON, check the host.Get started
Two headers authenticate every request.
| Header | Value |
|---|---|
X-API-KEY | Your API key |
X-API-SECRET | Your API secret, also called the API token |
Content-Type | application/json |
Both values come from API Settings in your Qpro+ account. The secret is visible only when you generate it; store it straight away. Regenerating it invalidates the previous secret immediately, so update every system that uses it at the same time.
Keep both values on your server. Print Markups is the one exception, and it's covered in its section.
Get started
Every endpoint takes the same three fields.
You name the template, say how many copies, and send the field values. Qpro+ resolves the template saved in your account under that name, so a change made in the canvas reaches the very next request without a code change.
{
"label_name": "FG-PALLET-4x6",
"amount": 1,
"apiData": {
"lot_number": "L25-8814",
"pallet_id": "PLT-4471",
"piece_qty": "48 CS",
"customer_code": "NORTHFIELD"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
label_name | string | Yes | Exact name of the template in your account. Case-sensitive. |
amount | integer | Yes | Copies to produce, 1 or more. Each copy is returned or printed separately. |
apiData | object | Yes | Field values keyed by the field names mapped on the template. Keys are case-sensitive; values are strings. |
Three endpoints add fields: print-node and print-node-pdf add printer_id; render-zpl adds dpi and max_width_dots.
Get started
Access, limits, and what counts as a print.
Plan
Production API access needs the Advanced plan. Sandbox trials include API access, so you can evaluate before you buy.
Rate limit
60 requests a minute per key by default. Over that you get 429.
Print allowance
Every render counts toward your plan, including Fetch Markups, Export SVG, Render ZPL and Render PDF.
Method 01 · Preview and debug
Fetch Markups resolves the label and returns it as JSON.
POST/custom-labels/fetch-markups
- Returns
- Markup JSON
- Delivered to
- Your app
- SDK
- Not required
- Counts as a print
- Yes
Qpro+ loads the template you name, fills every field from apiData, and returns the resolved label as markup: the stage size and every element with its final value. Nothing is rendered to a file and nothing prints.
Use it to confirm your field values resolve before you wire up any output, to feed a renderer of your own, or to inspect exactly what a template contains.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
label_name | string | Yes | Exact name of the template in your account. Case-sensitive. |
amount | integer | Yes | Copies to produce, 1 or more. Each copy is returned or printed separately. |
apiData | object | Yes | Field values keyed by the field names mapped on the template. Keys are case-sensitive; values are strings. |
Example request
curl -X POST "https://api.beta.quandosol.com/api/custom-labels/fetch-markups" \
-H "Content-Type: application/json" \
-H "X-API-KEY: $QPRO_API_KEY" \
-H "X-API-SECRET: $QPRO_API_SECRET" \
-d '{
"label_name": "FG-PALLET-4x6",
"amount": 1,
"apiData": {
"lot_number": "L25-8814",
"pallet_id": "PLT-4471",
"piece_qty": "48 CS",
"customer_code": "NORTHFIELD"
}
}'const res = await fetch("https://api.beta.quandosol.com/api/custom-labels/fetch-markups", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": process.env.QPRO_API_KEY,
"X-API-SECRET": process.env.QPRO_API_SECRET
},
body: JSON.stringify({
label_name: "FG-PALLET-4x6",
amount: 1,
apiData: {
lot_number: "L25-8814",
pallet_id: "PLT-4471",
piece_qty: "48 CS",
customer_code: "NORTHFIELD"
}
})
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const markups = await res.json(); // one entry per copy
import os, requests
res = requests.post(
"https://api.beta.quandosol.com/api/custom-labels/fetch-markups",
headers={
"X-API-KEY": os.environ["QPRO_API_KEY"],
"X-API-SECRET": os.environ["QPRO_API_SECRET"],
},
json={
"label_name": "FG-PALLET-4x6",
"amount": 1,
"apiData": {
"lot_number": "L25-8814",
"pallet_id": "PLT-4471",
"piece_qty": "48 CS",
"customer_code": "NORTHFIELD"
}
},
timeout=15,
)
res.raise_for_status()
markups = res.json() # one entry per copy
Response
[
{
"stage": {
"width": 4,
"height": 6,
"unit": "in",
"background": "#ffffff"
},
"elements": [
{
"type": "text",
"text": "L25-8814",
"x": 20,
"y": 15
},
{
"type": "barcode",
"format": "code128",
"text": "PLT-4471"
},
{
"type": "image",
"url": "https://\u2026",
"x": 10,
"y": 80
}
]
}
]
Trimmed for length. Each element carries its full geometry and styling.
| Field | Type | Description |
|---|---|---|
[ ] | array | One markup object per copy requested in amount. |
stage | object | Label size and background: width, height, unit, background. |
elements | array | Every element on the template with its resolved value: text, barcode, image, shapes, lines. |
Method 02 · Browser printing
Print Markups renders the label in the browser and opens the print dialog.
POST/custom-labels/print
- Returns
- Browser print dialog
- Delivered to
- Any printer the browser sees
- SDK
- Required
- Counts as a print
- Yes
The Qpro+ SDK calls this route, receives the same markup as Fetch Markups, draws the label to a canvas inside a hidden iframe, and triggers the browser's native print dialog. Choosing “Save as PDF” in that dialog produces a PDF file.
This is the one method designed to run in a browser. It needs no PrintNode account and no print server.
print-label.html at the path set in printPageUrl, and bwip-js loaded inside print-label.html for barcodes.Request body
| Field | Type | Required | Description |
|---|---|---|---|
label_name | string | Yes | Exact name of the template in your account. Case-sensitive. |
amount | integer | Yes | Copies to produce, 1 or more. Each copy is returned or printed separately. |
apiData | object | Yes | Field values keyed by the field names mapped on the template. Keys are case-sensitive; values are strings. |
Example request
<!-- 1. On the page that prints -->
<script src="https://beta.quandosol.com/qpro-label-sdk.js"></script>
<!-- 2. print-label.html, served from your site.
The SDK loads it into a hidden iframe to render the label. -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Print Label</title>
<style>
html, body { margin: 0; padding: 0; background: white; }
#printRoot { width: 100%; height: 100%; background: white; }
img { display: block; }
</style>
</head>
<body>
<div id="printRoot"></div>
<script src="https://unpkg.com/bwip-js/dist/bwip-js-min.js"></script>
<script src="https://beta.quandosol.com/qpro-label-sdk.js"></script>
</body>
</html>QPROLabelSDK.setConfig({
apiBaseUrl: "https://api.beta.quandosol.com/api",
api_key: "YOUR_API_KEY",
api_token: "YOUR_API_SECRET",
printPageUrl: "./print-label.html",
debug: false // true while developing
});
QPROLabelSDK.printLabel({
label_name: "FG-PALLET-4x6",
amount: 1,
apiData: {
lot_number: "L25-8814",
pallet_id: "PLT-4471",
piece_qty: "48 CS",
customer_code: "NORTHFIELD"
},
mode: "print_markups"
});curl -X POST "https://api.beta.quandosol.com/api/custom-labels/print" \
-H "Content-Type: application/json" \
-H "X-API-KEY: $QPRO_API_KEY" \
-H "X-API-SECRET: $QPRO_API_SECRET" \
-d '{
"label_name": "FG-PALLET-4x6",
"amount": 1,
"apiData": {
"lot_number": "L25-8814",
"pallet_id": "PLT-4471",
"piece_qty": "48 CS",
"customer_code": "NORTHFIELD"
}
}'Supported elements
| Element | Supported |
|---|---|
| Text, multi-line with wrapping | Yes |
| Images | Yes |
| Barcodes: Code 128, Code 39, EAN and more | Yes |
| QR Code | Yes |
| Shapes: rect, circle, ellipse, arc, ring, wedge | Yes |
| Lines | Yes |
Response
Called directly, the route returns the same markup array as Fetch Markups. The SDK does the rendering.
Watch out
Because this method runs in the browser, the key and secret passed to setConfig are readable by anyone who can open the page. Use it on internal, signed-in pages, and use a server-side method anywhere the page is public.
Method 03 · Thermal printing
PrintNode ZPL sends the label to a Zebra printer as ZPL.
POST/custom-labels/print-node
- Returns
- Status JSON
- Delivered to
- ZPL printer via PrintNode
- SDK
- Not required
- Counts as a print
- Yes
Qpro+ renders the label server-side, converts it to ZPL, and delivers it through PrintNode to a Zebra or other ZPL-compatible thermal printer. No browser is involved, which makes this the method for background and automated printing.
PrintNode is only the courier. The ZPL is produced by Qpro+, and it asks PrintNode for the printer's resolution so the output matches the hardware.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
label_name | string | Yes | Exact name of the template in your account. Case-sensitive. |
amount | integer | Yes | Copies to produce, 1 or more. Each copy is returned or printed separately. |
apiData | object | Yes | Field values keyed by the field names mapped on the template. Keys are case-sensitive; values are strings. |
printer_id | integer | Yes | PrintNode printer ID, copied from the Printers page of your PrintNode dashboard. |
Example request
curl -X POST "https://api.beta.quandosol.com/api/custom-labels/print-node" \
-H "Content-Type: application/json" \
-H "X-API-KEY: $QPRO_API_KEY" \
-H "X-API-SECRET: $QPRO_API_SECRET" \
-d '{
"label_name": "FG-PALLET-4x6",
"amount": 1,
"printer_id": 75208784,
"apiData": {
"lot_number": "L25-8814",
"pallet_id": "PLT-4471",
"piece_qty": "48 CS",
"customer_code": "NORTHFIELD"
}
}'const res = await fetch("https://api.beta.quandosol.com/api/custom-labels/print-node", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": process.env.QPRO_API_KEY,
"X-API-SECRET": process.env.QPRO_API_SECRET
},
body: JSON.stringify({
label_name: "FG-PALLET-4x6",
amount: 1,
printer_id: 75208784,
apiData: {
lot_number: "L25-8814",
pallet_id: "PLT-4471",
piece_qty: "48 CS",
customer_code: "NORTHFIELD"
}
})
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { status, printer_dpi } = await res.json();
import os, requests
res = requests.post(
"https://api.beta.quandosol.com/api/custom-labels/print-node",
headers={
"X-API-KEY": os.environ["QPRO_API_KEY"],
"X-API-SECRET": os.environ["QPRO_API_SECRET"],
},
json={
"label_name": "FG-PALLET-4x6",
"amount": 1,
"printer_id": 75208784,
"apiData": {
"lot_number": "L25-8814",
"pallet_id": "PLT-4471",
"piece_qty": "48 CS",
"customer_code": "NORTHFIELD"
}
},
timeout=15,
)
res.raise_for_status()
print(res.json()["status"]) # "sent_to_printer"
Response
{
"status": "sent_to_printer",
"printer_dpi": 203
}
| Field | Type | Description |
|---|---|---|
status | string | sent_to_printer when PrintNode accepted the job. |
printer_dpi | integer | Resolution PrintNode reported for the printer, used for the render. |
What the ZPL contains
The label is rasterised at the requested density and carried as a single ^GFA graphic field, with ^PW and ^LL set to match it exactly. Fonts, symbologies and artwork are resolved before the ZPL leaves Qpro+, so the printer has nothing to look up. The trade-off: the commands are not editable text or barcode fields.
| Command | Meaning |
|---|---|
^PW | Print width in dots |
^LL | Label length in dots |
^FO | Field origin, x and y |
^GFA | Graphic field carrying the rendered label |
Method 04 · Office and laser printing
PrintNode PDF sends a vector PDF to any printer.
POST/custom-labels/print-node-pdf
- Returns
- Status JSON
- Delivered to
- Any printer via PrintNode
- SDK
- Not required
- Counts as a print
- Yes
Qpro+ builds a vector PDF of the label server-side and delivers it through PrintNode to any printer that accepts PDF. It gives the sharpest output for labels and documents with graphics, and it works on standard office and laser printers.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
label_name | string | Yes | Exact name of the template in your account. Case-sensitive. |
amount | integer | Yes | Copies to produce, 1 or more. Each copy is returned or printed separately. |
apiData | object | Yes | Field values keyed by the field names mapped on the template. Keys are case-sensitive; values are strings. |
printer_id | integer | Yes | PrintNode printer ID, copied from the Printers page of your PrintNode dashboard. |
Example request
curl -X POST "https://api.beta.quandosol.com/api/custom-labels/print-node-pdf" \
-H "Content-Type: application/json" \
-H "X-API-KEY: $QPRO_API_KEY" \
-H "X-API-SECRET: $QPRO_API_SECRET" \
-d '{
"label_name": "FG-PALLET-4x6",
"amount": 1,
"printer_id": 75236019,
"apiData": {
"lot_number": "L25-8814",
"pallet_id": "PLT-4471",
"piece_qty": "48 CS",
"customer_code": "NORTHFIELD"
}
}'const res = await fetch("https://api.beta.quandosol.com/api/custom-labels/print-node-pdf", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": process.env.QPRO_API_KEY,
"X-API-SECRET": process.env.QPRO_API_SECRET
},
body: JSON.stringify({
label_name: "FG-PALLET-4x6",
amount: 1,
printer_id: 75236019,
apiData: {
lot_number: "L25-8814",
pallet_id: "PLT-4471",
piece_qty: "48 CS",
customer_code: "NORTHFIELD"
}
})
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { status } = await res.json();
import os, requests
res = requests.post(
"https://api.beta.quandosol.com/api/custom-labels/print-node-pdf",
headers={
"X-API-KEY": os.environ["QPRO_API_KEY"],
"X-API-SECRET": os.environ["QPRO_API_SECRET"],
},
json={
"label_name": "FG-PALLET-4x6",
"amount": 1,
"printer_id": 75236019,
"apiData": {
"lot_number": "L25-8814",
"pallet_id": "PLT-4471",
"piece_qty": "48 CS",
"customer_code": "NORTHFIELD"
}
},
timeout=15,
)
res.raise_for_status()
print(res.json()["status"])
Response
{
"status": "sent_to_printer_png"
}
The status string reads sent_to_printer_png even though the job is a PDF. Treat any 2xx as accepted.
| Field | Type | Description |
|---|---|---|
status | string | Confirms PrintNode accepted the job. |
Watch out
A PDF cannot carry an RFID tag payload, so a template with an RFID element is refused with 422. Print RFID labels with PrintNode ZPL or Render ZPL.
Method 05 · Stored vector files
Export SVG renders the label to SVG files and returns their URLs.
POST/custom-labels/export-svg
- Returns
- File URLs
- Delivered to
- Qpro+ storage, 24 h
- SDK
- Not required
- Counts as a print
- Yes
Qpro+ resolves the template, renders one SVG per copy on the server, stores the files, and returns a URL for each. The files expire after the period given in ttl_hours, 24 hours today, so download what you need to keep.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
label_name | string | Yes | Exact name of the template in your account. Case-sensitive. |
amount | integer | Yes | Copies to produce, 1 or more. Each copy is returned or printed separately. |
apiData | object | Yes | Field values keyed by the field names mapped on the template. Keys are case-sensitive; values are strings. |
Example request
curl -X POST "https://api.beta.quandosol.com/api/custom-labels/export-svg" \
-H "Content-Type: application/json" \
-H "X-API-KEY: $QPRO_API_KEY" \
-H "X-API-SECRET: $QPRO_API_SECRET" \
-d '{
"label_name": "FG-PALLET-4x6",
"amount": 1,
"apiData": {
"lot_number": "L25-8814",
"pallet_id": "PLT-4471",
"piece_qty": "48 CS",
"customer_code": "NORTHFIELD"
}
}' \
| jq -r '.files[].url'const res = await fetch("https://api.beta.quandosol.com/api/custom-labels/export-svg", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": process.env.QPRO_API_KEY,
"X-API-SECRET": process.env.QPRO_API_SECRET
},
body: JSON.stringify({
label_name: "FG-PALLET-4x6",
amount: 1,
apiData: {
lot_number: "L25-8814",
pallet_id: "PLT-4471",
piece_qty: "48 CS",
customer_code: "NORTHFIELD"
}
})
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { files } = await res.json();
// Each URL stays live until files[i].expires_at
const svg = await (await fetch(files[0].url)).text();
import os, requests
res = requests.post(
"https://api.beta.quandosol.com/api/custom-labels/export-svg",
headers={
"X-API-KEY": os.environ["QPRO_API_KEY"],
"X-API-SECRET": os.environ["QPRO_API_SECRET"],
},
json={
"label_name": "FG-PALLET-4x6",
"amount": 1,
"apiData": {
"lot_number": "L25-8814",
"pallet_id": "PLT-4471",
"piece_qty": "48 CS",
"customer_code": "NORTHFIELD"
}
},
timeout=15,
)
res.raise_for_status()
files = res.json()["files"]
svg = requests.get(files[0]["url"], timeout=15).text
Response
{
"status": "svg_exported",
"ttl_hours": 24,
"expires_at": "2026-10-01T12:00:00+00:00",
"files": [
{
"url": "https://api.beta.quandosol.com/storage/custom-label-svgs/2026/09/30/fg-pallet-4x6-uuid.svg",
"path": "custom-label-svgs/2026/09/30/fg-pallet-4x6-uuid.svg",
"expires_at": "2026-10-01T12:00:00+00:00"
}
]
}
| Field | Type | Description |
|---|---|---|
status | string | svg_exported on success. |
ttl_hours | integer | Hours each file stays available. |
expires_at | string | ISO 8601 time the batch expires. |
files[].url | string | Public URL of one SVG, one per copy. |
files[].path | string | Storage path of the same file. |
files[].expires_at | string | ISO 8601 time that file expires. |
Watch out
Anyone holding a file URL can open it until it expires. If a label carries personal data, keep the URLs server-side and hand your users your own copy.
Method 06 · Your own print path
Render ZPL returns the ZPL to your application.
POST/custom-labels/render-zpl
- Returns
- ZPL strings
- Delivered to
- Your app
- SDK
- Not required
- Counts as a print
- Yes
The same ZPL as PrintNode ZPL, handed back to you instead of sent to a printer. Use it when you already relay print jobs to your own printers: a raw socket on port 9100, a CUPS raw queue, or an existing print relay. No PrintNode account is involved.
There is no printer to ask for its size here, so tell Qpro+ with dpi and max_width_dots. Wrong values don't raise an error; they print a label that is too wide or too small.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
label_name | string | Yes | Exact name of the template in your account. Case-sensitive. |
amount | integer | Yes | Copies to produce, 1 or more. Each copy is returned or printed separately. |
apiData | object | Yes | Field values keyed by the field names mapped on the template. Keys are case-sensitive; values are strings. |
dpi | integer | No | Printer resolution, 100–1200. Default 203. |
max_width_dots | integer | No | Printhead width in dots, 100–10000. Default 832 (4.10 in at 203 dpi). |
Example request
curl -X POST "https://api.beta.quandosol.com/api/custom-labels/render-zpl" \
-H "Content-Type: application/json" \
-H "X-API-KEY: $QPRO_API_KEY" \
-H "X-API-SECRET: $QPRO_API_SECRET" \
-d '{
"label_name": "FG-PALLET-4x6",
"amount": 1,
"dpi": 300,
"max_width_dots": 1248,
"apiData": {
"lot_number": "L25-8814",
"pallet_id": "PLT-4471",
"piece_qty": "48 CS",
"customer_code": "NORTHFIELD"
}
}' \
| jq -r '.labels[0]' | nc 192.168.1.50 9100const res = await fetch("https://api.beta.quandosol.com/api/custom-labels/render-zpl", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": process.env.QPRO_API_KEY,
"X-API-SECRET": process.env.QPRO_API_SECRET
},
body: JSON.stringify({
label_name: "FG-PALLET-4x6",
amount: 1,
dpi: 300,
max_width_dots: 1248,
apiData: {
lot_number: "L25-8814",
pallet_id: "PLT-4471",
piece_qty: "48 CS",
customer_code: "NORTHFIELD"
}
})
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { labels } = await res.json();
// Hand each copy to your own printer endpoint
for (const zpl of labels) await sendToPrinter(zpl);
import os, requests
res = requests.post(
"https://api.beta.quandosol.com/api/custom-labels/render-zpl",
headers={
"X-API-KEY": os.environ["QPRO_API_KEY"],
"X-API-SECRET": os.environ["QPRO_API_SECRET"],
},
json={
"label_name": "FG-PALLET-4x6",
"amount": 1,
"dpi": 300,
"max_width_dots": 1248,
"apiData": {
"lot_number": "L25-8814",
"pallet_id": "PLT-4471",
"piece_qty": "48 CS",
"customer_code": "NORTHFIELD"
}
},
timeout=15,
)
res.raise_for_status()
import socket
for zpl in res.json()["labels"]:
with socket.create_connection(("192.168.1.50", 9100), timeout=10) as s:
s.sendall(zpl.encode("ascii"))
Response
{
"dpi": 300,
"max_width_dots": 1248,
"count": 1,
"labels": [
"^XA\n^PW1248\n^LL1800\n^LH0,0\n^FO0,0\n^GFA,\u2026^FS\n^XZ\n"
]
}
| Field | Type | Description |
|---|---|---|
dpi | integer | Resolution the label was rendered at. |
max_width_dots | integer | Printhead width used. |
count | integer | Number of labels returned, equal to amount. |
labels | string[] | One complete ZPL job per copy, ^XA to ^XZ. |
What the ZPL contains
The label is rasterised at the requested density and carried as a single ^GFA graphic field, with ^PW and ^LL set to match it exactly. Fonts, symbologies and artwork are resolved before the ZPL leaves Qpro+, so the printer has nothing to look up. The trade-off: the commands are not editable text or barcode fields.
| Command | Meaning |
|---|---|
^PW | Print width in dots |
^LL | Label length in dots |
^FO | Field origin, x and y |
^GFA | Graphic field carrying the rendered label |
Method 07 · Archive, email, queue
Render PDF returns the PDF to your application.
POST/custom-labels/render-pdf
- Returns
- Base64 PDF
- Delivered to
- Your app
- SDK
- Not required
- Counts as a print
- Yes
The same vector PDF as PrintNode PDF, returned base64-encoded in the JSON response instead of sent to a printer. The file is built entirely inside Qpro+, with no headless browser and no outside service in the path, so you can print it, archive it, attach it to a shipment, or email it.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
label_name | string | Yes | Exact name of the template in your account. Case-sensitive. |
amount | integer | Yes | Copies to produce, 1 or more. Each copy is returned or printed separately. |
apiData | object | Yes | Field values keyed by the field names mapped on the template. Keys are case-sensitive; values are strings. |
Example request
curl -X POST "https://api.beta.quandosol.com/api/custom-labels/render-pdf" \
-H "Content-Type: application/json" \
-H "X-API-KEY: $QPRO_API_KEY" \
-H "X-API-SECRET: $QPRO_API_SECRET" \
-d '{
"label_name": "FG-PALLET-4x6",
"amount": 1,
"apiData": {
"lot_number": "L25-8814",
"pallet_id": "PLT-4471",
"piece_qty": "48 CS",
"customer_code": "NORTHFIELD"
}
}' \
| jq -r '.labels[0]' | base64 --decode > label.pdfimport fs from "node:fs";
const res = await fetch("https://api.beta.quandosol.com/api/custom-labels/render-pdf", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": process.env.QPRO_API_KEY,
"X-API-SECRET": process.env.QPRO_API_SECRET
},
body: JSON.stringify({
label_name: "FG-PALLET-4x6",
amount: 1,
apiData: {
lot_number: "L25-8814",
pallet_id: "PLT-4471",
piece_qty: "48 CS",
customer_code: "NORTHFIELD"
}
})
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { labels } = await res.json();
labels.forEach((b64, i) =>
fs.writeFileSync(`label-${i + 1}.pdf`, Buffer.from(b64, "base64"))
);
import os, requests
import base64
res = requests.post(
"https://api.beta.quandosol.com/api/custom-labels/render-pdf",
headers={
"X-API-KEY": os.environ["QPRO_API_KEY"],
"X-API-SECRET": os.environ["QPRO_API_SECRET"],
},
json={
"label_name": "FG-PALLET-4x6",
"amount": 1,
"apiData": {
"lot_number": "L25-8814",
"pallet_id": "PLT-4471",
"piece_qty": "48 CS",
"customer_code": "NORTHFIELD"
}
},
timeout=15,
)
res.raise_for_status()
for i, b64 in enumerate(res.json()["labels"], 1):
with open(f"label-{i}.pdf", "wb") as f:
f.write(base64.b64decode(b64))
Response
{
"mime": "application/pdf",
"encoding": "base64",
"count": 1,
"labels": [
"JVBERi0xLjcKJeLjz9M\u2026"
]
}
| Field | Type | Description |
|---|---|---|
mime | string | Always application/pdf. |
encoding | string | Always base64. |
count | integer | Number of PDFs returned, equal to amount. |
labels | string[] | One base64-encoded PDF per copy. |
Watch out
RFID templates are refused with 422 and the message “RFID labels must be rendered as ZPL. PDF cannot encode RFID tags.” Use Render ZPL for those.
Reference
Choose by where the output needs to go.
| Your situation | Method |
|---|---|
| Check that your field values resolve before printing | 01 · Fetch Markups |
| Build your own renderer or preview | 01 · Fetch Markups |
| Print from a web application through the browser dialog | 02 · Print Markups |
| Print to a Zebra or thermal printer from a server | 03 · PrintNode ZPL |
| Background or automated warehouse printing | 03 · PrintNode ZPL |
| Print to an office or laser printer | 04 · PrintNode PDF |
| Labels and documents with complex graphics | 04 · PrintNode PDF |
| Get SVG files generated and hosted for you | 05 · Export SVG |
| Send ZPL to your own printers, with no PrintNode account | 06 · Render ZPL |
| Keep the PDF itself: archive, email, attach, or queue | 07 · Render PDF |
Reference
PrintNode setup is needed for methods 03 and 04 only.
Render ZPL and Render PDF return the label to you, and the other methods don't print server-side, so none of them need PrintNode.
- Install the PrintNode client from printnode.com/download on the computer the printer is attached to.
- Start the client and connect the printer.
- In your PrintNode dashboard, open Printers and copy the printer's ID.
- Send that ID as
printer_idin the request.
Reference
Five status codes cover what goes wrong.
| Status | Meaning | Common cause | What to do |
|---|---|---|---|
401 | Unauthorized | Key or secret missing, wrong, or regenerated since you stored it. | Check both headers. Regenerating a token invalidates the old one. |
404 | Not Found | No template with that label_name in your account. | Names are case-sensitive. Copy the name from the template gallery. |
422 | Unprocessable Entity | A parameter is missing or invalid. Render ZPL and Render PDF also return 422, not 404, for an unknown label_name. Both PDF methods return 422 for RFID templates. | Read the message in the response body; it names the problem. |
429 | Too Many Requests | Over the per-minute limit for your key: 60 requests a minute by default. | Back off and retry. Ask us before load-testing toward production volume. |
500 | Server Error | The render failed on our side. | Retry once, then contact support with the label name and time of the request. |
Reference
Questions developers ask in the first hour.
Why does a field print its placeholder instead of my value?
Why do I get a 404 from one endpoint and a 422 from another for the same label name?
Why is my RFID label refused?
Why does my ZPL print too wide or too small?
Can I edit the text or barcodes inside the ZPL Qpro+ returns?
Nothing reaches my printer through PrintNode. What should I check?
Do test calls use up my print allowance?
Can I call the API from browser JavaScript?
Where do I get an API key and secret?
Reference
Six habits that keep integrations quiet.
Keep credentials server-side
Proxy calls through your backend. Never ship the secret in browser code, a mobile app, or firmware a customer can read.
Prove the data first
Use Fetch Markups while developing to confirm every field resolves before you wire up an output.
Validate before you send
Check apiData values in your own system, so a bad value is caught before it becomes a printed label.
Match the hardware
Send dpi and max_width_dots on Render ZPL for the printer you will print on.
Download what you keep
Export SVG files expire. Copy them to your own storage if you need them past ttl_hours.
Render what you mean to produce
Every render counts toward your plan. Don't call render endpoints in a test loop.
