Pular para o conteúdo

Convert ZPL to PDF in PHP

To convert ZPL to PDF in PHP, POST the ZPL with cURL to the OpenZPL render endpoint with the header Accept: application/pdf and write the response to a file. Check the HTTP status before using the file: errors come back as text that starts with ERROR:.

Atualizado 2026-10-20 · Renders made with the OpenZPL engine

Open in the viewer

What you need

  • PHP 7.4 or later with the cURL extension (php -m | grep curl).
  • The ZPL, the label size in inches and the printer density (8dpmm = 203 dpi).

The minimal request

<?php
$zpl = file_get_contents('label.zpl');
$ch = curl_init('https://api.openzpl.com/v1/printers/8dpmm/labels/4x2/');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $zpl,
    CURLOPT_HTTPHEADER => ['Accept: application/pdf'],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$pdf = curl_exec($ch);
if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) !== 200) {
    throw new RuntimeException($pdf);   // "ERROR: ..."
}
file_put_contents('label.pdf', $pdf);

Passing the ZPL as a string to CURLOPT_POSTFIELDS sends it as the raw body. Passing an array would send a form; if you want that, use ['file' => new CURLFile('label.zpl')], which the API also accepts.

Warehouse bin label with the location A12 03 in large type and a Code 39 barcode
4 × 2 in label at 203 dpi (8 dpmm) · 812 × 406 px · Open in the viewer
^XA
^FO20,20^GB772,366,4^FS
^FO40,40^A0N,30,30^FDWAREHOUSE BIN^FS
^FO40,85^A0N,110,110^FDA12 03^FS
^BY3,3,110
^FO40,215^B3N,N,110,N,N^FDA1203^FS
^XZ

A function with retries and no memory spike

CURLOPT_FILE writes the body straight to disk, so a 50-page PDF never sits in a PHP string. A header callback picks up Retry-After and X-Total-Count as they arrive.

<?php
function zpl_to_pdf(string $zpl, string $path, int $dpmm = 8, string $size = '4x6',
                    ?string $apiKey = null, int $retries = 3): int
{
    $url = "https://api.openzpl.com/v1/printers/{$dpmm}dpmm/labels/{$size}/";
    $headers = ['Accept: application/pdf', 'Content-Type: text/plain; charset=utf-8'];
    if ($apiKey) {
        $headers[] = "X-API-Key: $apiKey";
    }
    for ($attempt = 0; ; $attempt++) {
        $out = fopen($path, 'wb');
        $meta = [];
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $zpl,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_FILE => $out,            // the body goes straight to disk
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$meta) {
                if (preg_match('/^(retry-after|x-total-count):\s*(\d+)/i', $line, $m)) {
                    $meta[strtolower($m[1])] = (int) $m[2];
                }
                return strlen($line);
            },
        ]);
        $ok = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        $error = curl_error($ch);
        fclose($out);
        if ($ok === false) {
            throw new RuntimeException("Request failed: $error");
        }
        if (($status === 429 || $status === 503) && $attempt < $retries) {
            sleep($meta['retry-after'] ?? 1);
            continue;
        }
        if ($status !== 200) {
            $message = trim(file_get_contents($path));   // "ERROR: ..."
            unlink($path);
            throw new RuntimeException("HTTP $status: $message");
        }
        return $meta['x-total-count'] ?? 1;
    }
}

$count = zpl_to_pdf(file_get_contents('label.zpl'), 'label.pdf');
echo "label.pdf: $count label(s)\n";

Tested on PHP 8.5 with a four-label file: it printed label.pdf: 4 label(s). A 60-copy label threw HTTP 413: ERROR: The ZPL produces 60 labels; the maximum per request is 50 and left no half-written file behind. The code calls no curl_close(): it does nothing since PHP 8.0 and is deprecated in 8.5.

Serving the PDF to a browser

To send the label to the user instead of saving it, render it to a temporary file and stream it with header('Content-Type: application/pdf') and readfile(). Don't echo the cURL result before checking the status, or users download an error message named label.pdf.

Erros

StatusWhat it meansWhat to do
400A parameter in the URL is wrong, or a barcode's data is invalid.Read the text after ERROR: and fix the request. Don't retry it unchanged.
404The ZPL has no label, or the label index is out of range. X-Total-Count says how many labels there are.Check that the ZPL has complete ^XA … ^XZ blocks, or ask for an index below X-Total-Count.
406The Accept header asks for a format the API doesn't produce.Send image/png or application/pdf.
413O corpo está acima de 1 MB, ou o ZPL produz mais de 50 etiquetas.Split the job into several requests.
429A limit of your plan was hit: requests per second, or labels per UTC day.Wait the seconds in Retry-After, then retry.
503The label took more than 10 seconds to render, or the service is at capacity.Retry once after Retry-After; if it repeats, lower the density or split the label.

Limites

  • Endpoint: POST https://api.openzpl.com/v1/printers/8dpmm/labels/4x6/ with the ZPL as the request body. The density is 6dpmm, 8dpmm, 12dpmm or 24dpmm; the size is in inches, up to 15 per side.
  • Accept: application/pdf without a label index returns every label, one per page. Add an index (.../4x6/0/) to get a single label.
  • Free plan: no key, 3 requests per second and 5,000 labels per day per IP address. Every label counts, ^PQ copies and PDF pages included. See pricing.
  • Per request: up to 50 labels and 1 MB of ZPL.

Perguntas

Can I use Guzzle instead of cURL?

Yes. Send the ZPL as the body option with the Accept: application/pdf header and use the sink option to write the response to a file. The API doesn't care which client you use.

Why does my PDF have question marks instead of accents?

Send the ZPL as UTF-8 and start the label with ^CI28. Without ^CI28 the printer, and the renderer, read the bytes in another character set.

Do I need an API key?

Not to start. Without a key the free limits apply per IP address. Add X-API-Key when you need your plan's limits.

Sources