← Back to Blog
Developers & API

Generate ZPL Labels with Node.js: Code Samples and API Integration Guide

Emily FletcherOctober 14, 2026537 views

A node label api integration lets your Node.js backend call the Label Toolkit REST API to generate production-ready ZPL strings, which you can spool directly to a Zebra printer over TCP port 9100 or forward to a print queue, all without running a local design tool. Label Toolkit provides a free, no-install endpoint that accepts JSON and returns ZPL II, the native language understood by every Zebra thermal printer built after 1990.

  • Label Toolkit exposes a public REST endpoint that accepts a label template ID plus variable field values and returns a ZPL string ready to send to any Zebra printer.
  • The Node.js https or fetch module (Node 18+) is sufficient; no SDK is required, though the samples below show both approaches.
  • A single API call can generate one label or a batch of hundreds by passing an array of data rows.
  • ZPL output from the API can be forwarded to a Zebra printer over a raw TCP socket on port 9100 or written to a file for offline use.

What the Label Toolkit API returns

Before writing code, it helps to understand what you are actually receiving. The API response contains a ZPL II string, a text-based page description language specific to Zebra printers. A minimal label might look like this:

^XA
^FO50,50^A0N,30,30^FDOrder #1042^FS
^FO50,100^BCN,80,Y,N,N^FD123456789012^FS
^XZ

Breaking that down: ^XA opens the label format; ^FO50,50 sets the field origin to x=50 dots, y=50 dots from the top-left corner; ^A0N,30,30 selects the scalable font at 30 by 30 dots; ^FD carries the field data; ^FS closes the field; ^BCN,80,Y,N,N renders a Code 128 barcode 80 dots tall with a human-readable interpretation line below; and ^XZ ends the format and triggers the print. Each of those commands is covered individually in the ZPL commands complete Zebra printing reference if you need to customize the output further.

At 203 DPI (dots per inch), the standard resolution for entry-level Zebra printers such as the ZD420, one dot equals approximately 0.125 mm. At 300 DPI, one dot equals roughly 0.085 mm. That matters when you are converting field origin coordinates from millimeters: multiply mm by 8 for 203 DPI, or by 11.8 for 300 DPI.

A diagram showing a 4x2 inch thermal label with annotated ZPL coordinate axes (dot units) and labeled regions for text field, barcode field, and quiet zone margin

Prerequisites

You need three things before writing a single line of Node.js:

  1. A Label Toolkit account and API key. Create a free account at /register, open any saved label, and copy the template ID from the label settings panel. Your API key lives in your account settings.
  2. Node.js 18 or later. Version 18 introduced native fetch, which keeps these samples dependency-free. If you are on Node 16, swap fetch for the node-fetch npm package or the built-in https module.
  3. A Zebra printer on your network (optional for testing). If you do not have a printer available yet, paste the returned ZPL string into the online ZPL viewer at /designer.html to preview the rendered label instantly in your browser.

Quick start: generate a single ZPL label

The following sample uses the native fetch API (Node 18+) to request a ZPL label string from Label Toolkit. Replace YOUR_API_KEY and YOUR_TEMPLATE_ID with real values from your account.

// generate-label.mjs
import { writeFileSync } from 'fs';

const API_KEY = 'YOUR_API_KEY';
const TEMPLATE_ID = 'YOUR_TEMPLATE_ID';
const BASE_URL = 'https://labeltoolkit.com/api/v1';

async function generateLabel(fields) {
  const response = await fetch(`${BASE_URL}/labels/${TEMPLATE_ID}/zpl`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}`,
    },
    body: JSON.stringify({ fields }),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`API error ${response.status}: ${error.message}`);
  }

  const { zpl } = await response.json();
  return zpl;
}

// Example: a shipping label with a Code 128 barcode
const zpl = await generateLabel({
  recipient_name: 'Jane Smith',
  address_line1: '742 Evergreen Terrace',
  city: 'Springfield',
  tracking_number: '1Z9999AA0123456784',
});

console.log(zpl);
// Optionally write to file for offline testing
writeFileSync('label.zpl', zpl, 'utf8');

Run it with node generate-label.mjs. The console output is a valid ZPL string you can send straight to a printer.

Send the ZPL directly to a Zebra printer over TCP

Zebra printers with an Ethernet or Wi-Fi interface listen on TCP port 9100 by default. The net module in Node.js lets you open a raw socket and stream ZPL directly, with no print driver required. This is the approach used in warehouse automation systems, where label printing latency needs to be under 200 ms.

// print-to-zebra.mjs
import net from 'net';

function printZpl(zplString, printerIp, printerPort = 9100) {
  return new Promise((resolve, reject) => {
    const socket = new net.Socket();

    socket.connect(printerPort, printerIp, () => {
      socket.write(zplString, 'utf8', () => {
        socket.end();
      });
    });

    socket.on('close', resolve);
    socket.on('error', reject);

    // Timeout after 5 seconds if printer is unreachable
    socket.setTimeout(5000, () => {
      socket.destroy();
      reject(new Error('Printer connection timed out'));
    });
  });
}

// Usage with the label generated above
await printZpl(zpl, '192.168.1.105');
console.log('Label sent to printer.');

If the printer is at a different IP, change 192.168.1.105 to the address shown on the printer's network configuration printout (press and hold the feed button for two seconds on most Zebra models to print a config page).

Bulk label generation from an array of records

Shipping and warehouse workflows almost always need more than one label at a time. The API accepts a records array, and you can also loop on the client side. Both patterns work; looping on the client side is preferable when each record needs independent error handling, while a server-side batch call reduces round trips for simpler use cases.

Client-side loop with concurrency control

// bulk-labels.mjs
import { writeFileSync } from 'fs';

const orders = [
  { recipient_name: 'Alice Chen', tracking_number: 'TRACK001', city: 'Austin' },
  { recipient_name: 'Bob Patel', tracking_number: 'TRACK002', city: 'Denver' },
  { recipient_name: 'Carol Liu',  tracking_number: 'TRACK003', city: 'Miami'  },
];

// Run up to 5 requests in parallel to stay within API rate limits
async function chunkGenerate(records, concurrency = 5) {
  const results = [];
  for (let i = 0; i < records.length; i += concurrency) {
    const slice = records.slice(i, i + concurrency);
    const batch = await Promise.all(slice.map(fields => generateLabel(fields)));
    results.push(...batch);
  }
  return results;
}

const labels = await chunkGenerate(orders);
// Concatenate all ZPL strings; a Zebra printer prints them in sequence
const combinedZpl = labels.join('\n');
writeFileSync('bulk-labels.zpl', combinedZpl, 'utf8');
console.log(`Generated ${labels.length} labels.`);

When you concatenate ZPL strings, each label begins with ^XA and ends with ^XZ, so the printer treats them as separate jobs and advances media between them automatically. For detailed guidance on label API quickstart and REST authentication patterns, see the full API reference article.

Adding barcodes and QR codes via field values

Label Toolkit templates support barcode fields, so you do not need to hand-write ^BC or ^BQ ZPL commands in your Node.js code. You define the barcode type once in the visual designer, then supply only the data string at request time.

Barcode typeZPL commandTypical data lengthCommon use case
Code 128^BCN,80,Y,N,NVariable (1 to 48 chars)Shipping, internal tracking
QR Code^BQN,2,4Up to ~2953 bytes (binary)URLs, serialized product data
GS1-128^BCN,80,Y,N,N with ^FD>8 prefixVariable with GS1 AIsRetail, pharma compliance
ITF-14^B2N,80,Y14 digits exactlyOuter carton / GTIN-14
Data Matrix^BXN,3,200Up to 2335 alphanumeric charsPCB, pharma, tiny labels

If you need to modify barcode parameters directly in the ZPL output, the ^BCN,80,Y,N,N command breaks down as: orientation N (normal), bar code height 80 dots, print interpretation line Y (yes), print interpretation line above N (no), check digit N (auto-calculated). The Zebra ZPL II Programming Guide (available from Zebra's official documentation portal) documents every parameter for every barcode command.

Error handling and retry logic

Network calls fail. Printers go offline. A production integration needs at least basic retry logic with exponential back-off to avoid hammering the API during a transient outage.

// with-retry.mjs
async function generateLabelWithRetry(fields, maxAttempts = 3) {
  let lastError;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await generateLabel(fields);
    } catch (err) {
      lastError = err;
      if (attempt < maxAttempts) {
        // Wait 200ms, 400ms, 800ms... before retrying
        const delay = 200 * Math.pow(2, attempt - 1);
        console.warn(`Attempt ${attempt} failed. Retrying in ${delay}ms...`);
        await new Promise(res => setTimeout(res, delay));
      }
    }
  }
  throw lastError;
}

Common HTTP status codes you will encounter from the API:

StatusMeaningRecommended action
200Success, ZPL returnedPrint or store the ZPL string
400Bad request (missing required field)Check your fields object against the template definition
401Invalid or missing API keyVerify the Authorization: Bearer header
404Template ID not foundConfirm the template exists and is published
429Rate limit exceededReduce concurrency; use exponential back-off
500Server errorRetry with back-off; contact support if persistent

Using the Node.js https module for older runtimes

If you are stuck on Node 14 or 16 and cannot add npm dependencies, the built-in https module still gets the job done, just with more boilerplate.

// generate-label-https.cjs
const https = require('https');

function generateLabelHttps(fields) {
  return new Promise((resolve, reject) => {
    const body = JSON.stringify({
      fields,
      templateId: 'YOUR_TEMPLATE_ID',
    });

    const options = {
      hostname: 'labeltoolkit.com',
      path: '/api/v1/labels/YOUR_TEMPLATE_ID/zpl',
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer YOUR_API_KEY',
        'Content-Length': Buffer.byteLength(body),
      },
    };

    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', chunk => { data += chunk; });
      res.on('end', () => {
        if (res.statusCode === 200) {
          resolve(JSON.parse(data).zpl);
        } else {
          reject(new Error(`HTTP ${res.statusCode}: ${data}`));
        }
      });
    });

    req.on('error', reject);
    req.write(body);
    req.end();
  });
}

Validating ZPL output before printing

Before sending a newly generated ZPL string to a production Zebra printer, run a quick sanity check in Node.js to catch obvious format problems. Two things to verify: the string starts with ^XA and ends with ^XZ, and every ^FO field origin command is paired with a ^FS field separator. Missing ^FS is the most common cause of truncated fields on printed labels.

function validateZpl(zpl) {
  if (!zpl.startsWith('^XA')) throw new Error('ZPL missing ^XA label start');
  if (!zpl.endsWith('^XZ')) throw new Error('ZPL missing ^XZ label end');

  const foCount = (zpl.match(/\^FO/g) || []).length;
  const fsCount = (zpl.match(/\^FS/g) || []).length;
  if (foCount !== fsCount) {
    throw new Error(`Mismatched ^FO (${foCount}) and ^FS (${fsCount}) commands`);
  }

  return true;
}

For a deeper look at what each ZPL command does and how to write field definitions manually, the GS1 General Specifications (gs1.org) are the authoritative source for barcode data requirements that feed into your field values.

Complete end-to-end example

This final sample ties everything together: fetch ZPL from the API, validate it, then print to the network Zebra printer. Copy-paste this as your starting point for a production integration.

// end-to-end.mjs
import net from 'fs'; // for file fallback
import netSocket from 'net';

const API_KEY = process.env.LABEL_TOOLKIT_API_KEY;
const TEMPLATE_ID = process.env.LABEL_TOOLKIT_TEMPLATE_ID;
const PRINTER_IP = process.env.ZEBRA_PRINTER_IP || '192.168.1.105';

async function generateLabel(fields) {
  const res = await fetch(
    `https://labeltoolkit.com/api/v1/labels/${TEMPLATE_ID}/zpl`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${API_KEY}`,
      },
      body: JSON.stringify({ fields }),
    }
  );
  if (!res.ok) throw new Error(`API ${res.status}`);
  const { zpl } = await res.json();
  return zpl;
}

function validateZpl(zpl) {
  if (!zpl.startsWith('^XA') || !zpl.endsWith('^XZ')) {
    throw new Error('Invalid ZPL wrapper');
  }
}

function printZpl(zpl, ip, port = 9100) {
  return new Promise((resolve, reject) => {
    const sock = new netSocket.Socket();
    sock.connect(port, ip, () => sock.write(zpl, 'utf8', () => sock.end()));
    sock.on('close', resolve);
    sock.on('error', reject);
    sock.setTimeout(5000, () => { sock.destroy(); reject(new Error('Timeout')); });
  });
}

// Run
const fields = {
  sku: 'WH-9823',
  description: 'Widget Handle 90mm',
  quantity: '48',
  barcode_value: '00100123450000000011',
};

const zpl = await generateLabel(fields);
validateZpl(zpl);
await printZpl(zpl, PRINTER_IP);
console.log('Label printed successfully.');

Store your API key and template ID in environment variables (shown above as LABEL_TOOLKIT_API_KEY etc.) rather than hard-coding them. A .env file with the dotenv package, or the native --env-file flag available from Node 20.6+, keeps credentials out of source control.

A flow diagram showing Node.js app calling Label Toolkit REST API, receiving a ZPL string, validating it, then sending over TCP to a Zebra printer on port 9100

Frequently asked questions

Do I need an npm package to call the Label Toolkit label API from Node.js?

No. Node.js 18 and later includes native fetch, so you can call the REST API with zero dependencies. For older Node versions, the built-in https module works fine, or you can add node-fetch from npm if you prefer the fetch interface.

How do I send the generated ZPL to a Zebra printer from Node.js without a print driver?

Open a raw TCP socket to the printer's IP address on port 9100 using Node's built-in net module, write the ZPL string, then close the socket. The printer interprets the data immediately and prints the label. No driver, no spooler, no operating system print dialog needed.

What ZPL printer resolution should I target when building coordinates into my template?

Design for 203 DPI (8 dots per mm) for the broadest compatibility with entry-level Zebra printers like the ZD420 and GX430t. If your target printer is 300 DPI (11.8 dots per mm), adjust field origin values in the template accordingly. The Label Toolkit designer lets you set the target DPI before exporting ZPL.

Can I generate labels in bulk with a single API call?

Yes. Pass an array of field objects in the request body and the API returns an array of ZPL strings in the same order. Alternatively, loop on the client side with controlled concurrency, as shown in the bulk example above, which gives you finer-grained error handling per record.

How do I preview ZPL output without a physical Zebra printer?

Paste the ZPL string into the Label Toolkit online designer at /designer.html. It renders the label in your browser pixel-accurately. You can also use the ZPL import feature to turn the ZPL back into an editable label if you want to tweak field positions visually.

Ready to connect your Node.js app to a real label API? Create your free Label Toolkit account and grab your API key in under two minutes. No credit card, no install, no waiting.

#node label api #generate zpl via api #label generation rest api #automate label printing #bulk label api #nodejs zpl #developers api
EF
Written by Emily Fletcher
View profile →