<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jaap Elst</title>
    <description>The latest articles on DEV Community by Jaap Elst (@jaap_elst).</description>
    <link>https://dev.to/jaap_elst</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2006831%2F676baea3-ce7f-4527-a167-20f30055f594.png</url>
      <title>DEV Community: Jaap Elst</title>
      <link>https://dev.to/jaap_elst</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jaap_elst"/>
    <language>en</language>
    <item>
      <title>How to convert ZPL shipping labels to PDF without a Zebra printer</title>
      <dc:creator>Jaap Elst</dc:creator>
      <pubDate>Mon, 26 Oct 2026 09:03:38 +0000</pubDate>
      <link>https://dev.to/jaap_elst/how-to-convert-zpl-shipping-labels-to-pdf-without-a-zebra-printer-gpi</link>
      <guid>https://dev.to/jaap_elst/how-to-convert-zpl-shipping-labels-to-pdf-without-a-zebra-printer-gpi</guid>
      <description>&lt;h2&gt;
  
  
  Why you'd want this
&lt;/h2&gt;

&lt;p&gt;If you've integrated with a carrier like PostNL, DHL or DPD, you've probably received shipping labels as ZPL: plain text meant for a Zebra thermal printer. That's great when a Zebra printer sits next to you. It's annoying everywhere else.&lt;/p&gt;

&lt;p&gt;Some situations where you need ZPL as a PDF or image instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want to preview a label in your admin panel before printing it.&lt;/li&gt;
&lt;li&gt;Your warehouse has a regular office printer, not a thermal one.&lt;/li&gt;
&lt;li&gt;You need to archive labels or email them to a customer.&lt;/li&gt;
&lt;li&gt;You're debugging a label layout and don't want to waste label stock on every tweak.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this tutorial we'll convert ZPL to PDF and PNG with a single HTTP request, using &lt;a href="https://caretly.dev" rel="noopener noreferrer"&gt;Caretly&lt;/a&gt;, a ZPL rendering API I built. We'll do it with cURL, Node.js and Python, then look at batching and a few production tips.&lt;/p&gt;

&lt;h2&gt;
  
  
  ZPL in 30 seconds
&lt;/h2&gt;

&lt;p&gt;ZPL (Zebra Programming Language) describes a label as a list of commands. Each command starts with a caret (&lt;code&gt;^&lt;/code&gt;). Here's a small shipping label we'll use throughout this post. Save it as &lt;code&gt;label.zpl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;^XA
^CI28
^FO40,40^A0N,24,24^FDFrom: Demo Shop B.V.^FS
^FO40,150^A0N,48,48^FDJ. Jansen^FS
^FO40,206^A0N,36,36^FDVoorbeeldstraat 12^FS
^FO40,250^A0N,36,36^FD3511 AB Utrecht NL^FS
^FO40,340^BY3^BCN,150,Y,N,N
^FD3SDEMO012345678^FS
^XZ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commands you see here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;^XA&lt;/code&gt; and &lt;code&gt;^XZ&lt;/code&gt; mark the start and end of a label.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;^CI28&lt;/code&gt; switches to UTF-8, so characters like é and ü print correctly.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;^FO40,40&lt;/code&gt; sets the field origin: 40 dots from the left, 40 dots from the top.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;^A0N,24,24&lt;/code&gt; picks font 0, normal orientation, 24 dots high and wide.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;^FD...^FS&lt;/code&gt; holds the field data, the actual text.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;^BY3^BCN,150,Y,N,N&lt;/code&gt; draws a Code 128 barcode, 150 dots tall, with the human-readable text below it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Positions are in dots, not pixels or millimetres. A typical 203 dpi Zebra printer has 8 dots per millimetre, which matters when you choose an output resolution later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick preview: the browser playground
&lt;/h2&gt;

&lt;p&gt;If you just want to see what a label looks like, you don't need any code. Paste the ZPL above into the &lt;a href="https://caretly.dev/playground/" rel="noopener noreferrer"&gt;Caretly playground&lt;/a&gt; and the label renders as you type. Clicking a field on the rendered label jumps to the line of ZPL that drew it, which is handy when you're hunting for the command that puts a barcode 10 dots too far to the right.&lt;/p&gt;

&lt;p&gt;The playground is free and needs no account. Its output carries a PREVIEW watermark, so it's for checking layouts, not for printing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering ZPL with the API
&lt;/h2&gt;

&lt;p&gt;First, create a test key on the API keys page of the &lt;a href="https://caretly.dev/console" rel="noopener noreferrer"&gt;Caretly console&lt;/a&gt;. Test keys are free and stamp TEST on every label, so you can build your whole integration without paying anything. Export the key as an environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ZPL_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"zpl_test_..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API is one endpoint. You POST the raw ZPL as the request body and get a PDF back.&lt;/p&gt;

&lt;h3&gt;
  
  
  cURL
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://caretly.dev/render?format=pdf"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$ZPL_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: text/plain"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @label.zpl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; label.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;--data-binary&lt;/code&gt;, not &lt;code&gt;-d&lt;/code&gt;. Plain &lt;code&gt;-d&lt;/code&gt; strips newlines, which can break multi-line ZPL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Node.js
&lt;/h3&gt;

&lt;p&gt;This uses the built-in &lt;code&gt;fetch&lt;/code&gt; in Node 18 and later, so there's nothing to install.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writeFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zpl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;label.zpl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://caretly.dev/render?format=pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ZPL_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/plain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;zpl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Render failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;label.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Pages:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-Page-Count&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Python
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label.zpl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;zpl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://caretly.dev/render&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ZPL_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/plain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;zpl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pages:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Page-Count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If something goes wrong, the error body is plain text that says what to fix. A &lt;code&gt;401&lt;/code&gt; means the key is missing, invalid, expired or revoked.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;label.pdf&lt;/code&gt; and you'll see the label, barcode included, ready for any regular printer.&lt;/p&gt;

&lt;h3&gt;
  
  
  PNG instead of PDF
&lt;/h3&gt;

&lt;p&gt;For previews in a web page, an image is often easier to show than a PDF. Set &lt;code&gt;format=png&lt;/code&gt; and choose the resolution with &lt;code&gt;dpmm&lt;/code&gt;, in dots per millimetre:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://caretly.dev/render?format=png&amp;amp;dpmm=12"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$ZPL_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @label.zpl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; label.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick the &lt;code&gt;dpmm&lt;/code&gt; that matches the printer the ZPL was written for: 6 (152 dpi), 8 (203 dpi, the default), 12 (300 dpi) or 24 (600 dpi). ZPL positions are in dots, so the wrong value makes the label come out too big or too small.&lt;/p&gt;

&lt;p&gt;A PNG holds one page. If your ZPL contains several labels, pick one with &lt;code&gt;page&lt;/code&gt;, starting at 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going to production
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Many labels at once
&lt;/h3&gt;

&lt;p&gt;A ZPL file can hold many labels, one &lt;code&gt;^XA ... ^XZ&lt;/code&gt; block after another, and &lt;code&gt;^PQ&lt;/code&gt; adds copies. Send the whole file and you get back one PDF with a page per label. The &lt;code&gt;X-Page-Count&lt;/code&gt; header tells you how many pages you received.&lt;/p&gt;

&lt;p&gt;That's how you'd print a day's worth of parcels in one go. In a test with 500 real carrier labels, including bitmap logos, the server rendered them into a single PDF in about 0.4 seconds.&lt;/p&gt;

&lt;p&gt;If your labels come as separate files, upload them as &lt;code&gt;multipart/form-data&lt;/code&gt; with one &lt;code&gt;file&lt;/code&gt; part each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://caretly.dev/render?format=pdf"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$ZPL_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;@first.zpl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;@second.zpl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;@third.zpl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; labels.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pages follow the upload order, and the &lt;code&gt;X-File-Page-Counts&lt;/code&gt; header lists the pages per file, for example &lt;code&gt;2, 1, 1&lt;/code&gt;. Each file gets a fresh printer state, so settings like &lt;code&gt;^PW&lt;/code&gt; or stored graphics in one file don't leak into the next.&lt;/p&gt;

&lt;p&gt;Labels with embedded bitmaps get large but compress well. Add &lt;code&gt;Content-Encoding: gzip&lt;/code&gt; and send the body gzipped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;gzip&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; labels.zpl | curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://caretly.dev/render?format=pdf"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$ZPL_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Encoding: gzip"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @- &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; labels.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One request can carry up to 100 MiB (measured after decompression) and 1,000 pages. Split bigger batches over several requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safe retries with Idempotency-Key
&lt;/h3&gt;

&lt;p&gt;Networks fail. If your request times out, you don't know whether the label was rendered and billed. Send an &lt;code&gt;Idempotency-Key&lt;/code&gt; header, any string up to 255 characters. A retry with the same key, the same labels and the same query parameters is billed only once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://caretly.dev/render?format=pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ZPL_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/plain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`order-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;zpl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your order or shipment number makes a natural key, because it stays the same on every retry of that job.&lt;/p&gt;

&lt;p&gt;The API is also rate limited per key, at about 50 requests per second with bursts of 200. That's mainly there to stop runaway loops. If you hit it, you get a &lt;code&gt;429&lt;/code&gt; with a &lt;code&gt;Retry-After&lt;/code&gt; header saying how many seconds to wait.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keeping an eye on usage
&lt;/h3&gt;

&lt;p&gt;Every response includes &lt;code&gt;X-Labels-Billed&lt;/code&gt;: each page, &lt;code&gt;^PQ&lt;/code&gt; copies included, for a live key, 1 for a PNG, and 0 for a test key. Log it next to your order ID and you can reconcile your bill against your own records. There's also a &lt;a href="https://caretly.dev/docs/usage-api" rel="noopener noreferrer"&gt;usage API&lt;/a&gt; that returns monthly, daily and per-key totals as JSON, if you'd rather pull usage into a dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Going live
&lt;/h3&gt;

&lt;p&gt;When everything works with the test key, swap in a live key. Live labels cost €0.002 each, with no subscription. Labels are rendered in the EU and not stored, which keeps the GDPR conversation short when your labels contain customer addresses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Turning ZPL into a PDF or PNG takes one POST request: raw ZPL in, printable file out, no printer or driver involved. From there you can show label previews in your app, print on ordinary printers, or archive labels as PDFs.&lt;/p&gt;

&lt;p&gt;Useful links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://caretly.dev/playground/" rel="noopener noreferrer"&gt;Playground&lt;/a&gt;, to try your own ZPL in the browser&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://caretly.dev/docs" rel="noopener noreferrer"&gt;Docs&lt;/a&gt;, for the full API reference and supported ZPL commands&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://caretly.dev/console/register" rel="noopener noreferrer"&gt;Free test key&lt;/a&gt;, to start integrating&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I built Caretly because I kept needing this myself, so I'd love to hear how you handle labels. If you run into a ZPL command that doesn't render the way your printer does, leave a comment below.&lt;/p&gt;

</description>
      <category>zpl</category>
      <category>ecommerce</category>
      <category>api</category>
      <category>logistics</category>
    </item>
  </channel>
</rss>
