# Qpro+ API reference
> Qpro+ is a cloud label and document design and print API by Quando Solutions LLC. Templates are designed and stored in the Qpro+ canvas; your system sends a template name and field values at print time and gets back markup JSON, ZPL, PDF or SVG, or has the label printed through PrintNode.
- Human-readable page: https://quandopro.com/api-reference.html
- OpenAPI 3.1: https://quandopro.com/openapi.json
- Last updated: 2026-09-29
## Environments
| Environment | Base URL | Use |
|---|---|---|
| Sandbox | https://api.beta.quandosol.com/api | Sandbox accounts: evaluation and development |

Production behaves identically; its base URL is issued with production credentials. Switching means changing the base URL and key pair only.

All routes are under `/custom-labels/`. `beta.quandosol.com` serves the web app, not the API; a request there can return 200 with HTML.
## Authentication
| Header | Value |
|---|---|
| X-API-KEY | API key |
| X-API-SECRET | API secret (API token) |
| Content-Type | application/json |

Credentials come from API Settings in your Qpro+ account. The secret is shown once; regenerating it invalidates the previous one immediately. Keep both server-side.
## Request shape
Every endpoint takes this body:

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

`print-node` and `print-node-pdf` add `printer_id`. `render-zpl` adds `dpi` and `max_width_dots`.
## Access and limits
- Production API access is included on the Integrate plan. Sandbox trials include API access.
- Rate limit: 60 requests per minute per key by default; over it returns 429.
- Every render counts toward the plan's print allowance, including fetch-markups, export-svg, render-zpl and render-pdf.
## Endpoints at a glance
| # | Method | Route | Returns | Delivered to | SDK |
|---|---|---|---|---|---|
| 01 | Fetch Markups | POST /custom-labels/fetch-markups | Markup JSON | Your app | No |
| 02 | Print Markups | POST /custom-labels/print | Browser print dialog | Any printer the browser sees | Required |
| 03 | PrintNode ZPL | POST /custom-labels/print-node | Status JSON | ZPL printer via PrintNode | No |
| 04 | PrintNode PDF | POST /custom-labels/print-node-pdf | Status JSON | Any printer via PrintNode | No |
| 05 | Export SVG | POST /custom-labels/export-svg | File URLs | Qpro+ storage, 24 h | No |
| 06 | Render ZPL | POST /custom-labels/render-zpl | ZPL strings | Your app | No |
| 07 | Render PDF | POST /custom-labels/render-pdf | Base64 PDF | Your app | No |

## 01. Fetch Markups — `POST /custom-labels/fetch-markups`
Fetch Markups resolves the label and returns it as JSON.

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 (curl)

```bash
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"
    }
  }'
```

### Example (JavaScript)

```javascript
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
```

### Example (Python)

```python
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

```json
[
  {
    "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. |

## 02. Print Markups — `POST /custom-labels/print`
Print Markups renders the label in the browser and opens the print dialog.

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.

> Prerequisites. The SDK script on the printing page, 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 (HTML)

```html
<!-- 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>
```

### Example (JavaScript)

```javascript
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"
});
```

### Example (curl)

```bash
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.

## 03. PrintNode ZPL — `POST /custom-labels/print-node`
PrintNode ZPL sends the label to a Zebra printer as ZPL.

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.

> Prerequisites. The PrintNode client installed and running on the computer the printer is attached to, and the printer registered in your PrintNode dashboard. See Printer setup.

### 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 (curl)

```bash
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"
    }
  }'
```

### Example (JavaScript)

```javascript
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();
```

### Example (Python)

```python
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

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

The label is rasterised into a single `^GFA` graphic field with `^PW` and `^LL` set to match. Fonts, symbologies and artwork are resolved server-side; the ZPL is not editable text or barcode fields.

## 04. PrintNode PDF — `POST /custom-labels/print-node-pdf`
PrintNode PDF sends a vector PDF to any printer.

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.

> Prerequisites. Same as PrintNode ZPL: the PrintNode client running and the printer registered.

### 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 (curl)

```bash
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"
    }
  }'
```

### Example (JavaScript)

```javascript
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();
```

### Example (Python)

```python
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

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

## 05. Export SVG — `POST /custom-labels/export-svg`
Export SVG renders the label to SVG files and returns their URLs.

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 (curl)

```bash
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'
```

### Example (JavaScript)

```javascript
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();
```

### Example (Python)

```python
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

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

## 06. Render ZPL — `POST /custom-labels/render-zpl`
Render ZPL returns the ZPL to your application.

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 (curl)

```bash
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 9100
```

### Example (JavaScript)

```javascript
const 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);
```

### Example (Python)

```python
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

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

The label is rasterised into a single `^GFA` graphic field with `^PW` and `^LL` set to match. Fonts, symbologies and artwork are resolved server-side; the ZPL is not editable text or barcode fields.

## 07. Render PDF — `POST /custom-labels/render-pdf`
Render PDF returns the PDF to your application.

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 (curl)

```bash
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.pdf
```

### Example (JavaScript)

```javascript
import 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"))
);
```

### Example (Python)

```python
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

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

## Choosing a method

| Situation | Method |
|---|---|
| Check that your field values resolve before printing | 01 Fetch Markups (/custom-labels/fetch-markups) |
| Build your own renderer or preview | 01 Fetch Markups (/custom-labels/fetch-markups) |
| Print from a web application through the browser dialog | 02 Print Markups (/custom-labels/print) |
| Print to a Zebra or thermal printer from a server | 03 PrintNode ZPL (/custom-labels/print-node) |
| Background or automated warehouse printing | 03 PrintNode ZPL (/custom-labels/print-node) |
| Print to an office or laser printer | 04 PrintNode PDF (/custom-labels/print-node-pdf) |
| Labels and documents with complex graphics | 04 PrintNode PDF (/custom-labels/print-node-pdf) |
| Get SVG files generated and hosted for you | 05 Export SVG (/custom-labels/export-svg) |
| Send ZPL to your own printers, with no PrintNode account | 06 Render ZPL (/custom-labels/render-zpl) |
| Keep the PDF itself: archive, email, attach, or queue | 07 Render PDF (/custom-labels/render-pdf) |

## PrintNode setup (methods 03 and 04 only)

1. Install the PrintNode client (https://www.printnode.com/download) on the computer the printer is attached to.
2. Start the client and connect the printer.
3. In the PrintNode dashboard, open Printers and copy the printer ID.
4. Send it as `printer_id`.

## Errors

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

## Troubleshooting

**Why does a field print its placeholder instead of my value?**
The keys in apiData must match the field names on the template exactly, including capitalisation. A key that doesn't match is ignored: the field prints its placeholder instead of your value, and the response is still 200. Call Fetch Markups and check each element's value.

**Why do I get a 404 from one endpoint and a 422 from another for the same label name?**
Render ZPL and Render PDF report an unknown label_name as 422; the other methods report it as 404. Treat both as “template not found” in your error handling.

**Why is my RFID label refused?**
A PDF cannot carry an RFID tag payload, so PrintNode PDF and Render PDF return 422 for any template with an RFID element. PrintNode ZPL and Render ZPL both encode RFID.

**Why does my ZPL print too wide or too small?**
With Render ZPL there is no printer for Qpro+ to ask, so it uses dpi and max_width_dots from your request, defaulting to 203 dpi and 832 dots. Send the values for the printer you will print on.

**Can I edit the text or barcodes inside the ZPL Qpro+ returns?**
No. The label is rasterised into a single ^GFA graphic field with fonts, symbologies and artwork already resolved, so the printer has nothing to look up. To change the label, edit the template in the Qpro+ canvas; the next request uses the change.

**Nothing reaches my printer through PrintNode. What should I check?**
Confirm the PrintNode client is running on the computer the printer is attached to, that the printer shows online in your PrintNode dashboard, and that printer_id matches it.

**Do test calls use up my print allowance?**
Yes. Fetch Markups, Export SVG, Render ZPL and Render PDF each record a print, as the printing methods do. Develop against your sandbox account and render what you intend to produce.

**Can I call the API from browser JavaScript?**
Only Print Markups is designed for that, and it exposes your credentials to anyone who can open the page. Call every other method from your backend and pass the result to the browser.

**Where do I get an API key and secret?**
In your Qpro+ account under API Settings. The secret is shown once, when you generate it, so store it immediately. Regenerating it invalidates the previous one at once.

## Best practices

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