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

^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
| Status | What it means | What to do |
|---|---|---|
400 | A 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. |
404 | The 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. |
406 | The Accept header asks for a format the API doesn't produce. | Send image/png or application/pdf. |
413 | O corpo está acima de 1 MB, ou o ZPL produz mais de 50 etiquetas. | Split the job into several requests. |
429 | A limit of your plan was hit: requests per second, or labels per UTC day. | Wait the seconds in Retry-After, then retry. |
503 | The 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 is6dpmm,8dpmm,12dpmmor24dpmm; the size is in inches, up to 15 per side. Accept: application/pdfwithout 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,
^PQcopies 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.