# Convert ZPL to PDF in C#

Source: https://openzpl.com/guides/zpl-to-pdf-csharp/ (updated 2026-10-20)

To convert ZPL to PDF in C#, post the ZPL as StringContent with HttpClient to the OpenZPL render endpoint, ask for application/pdf, and copy the response stream to a file. Reuse one HttpClient for the whole application and check the status code before saving.

## What you need

- Any supported .NET (6 or later). HttpClient is part of the base library; no NuGet package is needed.
- The ZPL as a string, the label size in inches and the printer density (8dpmm = 203 dpi).

## The code

HttpClient is meant to be created once and reused: a new client per label exhausts sockets under load. Ask for the headers first (ResponseHeadersRead) so the PDF is copied to the file as it arrives instead of being buffered.

```csharp
using System.Net;
using System.Net.Http.Headers;
using System.Text;

public static class LabelApi
{
    static readonly HttpClient Http = new()
    {
        BaseAddress = new Uri("https://api.openzpl.com/"),
        Timeout = TimeSpan.FromSeconds(30),
    };

    public static async Task<int> ZplToPdfAsync(string zpl, string path,
        int dpmm = 8, string size = "4x6", string? apiKey = null, int retries = 3)
    {
        for (var attempt = 0; ; attempt++)
        {
            using var req = new HttpRequestMessage(HttpMethod.Post, $"v1/printers/{dpmm}dpmm/labels/{size}/")
            {
                Content = new StringContent(zpl, Encoding.UTF8, "text/plain"),
            };
            req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/pdf"));
            if (apiKey is not null) req.Headers.Add("X-API-Key", apiKey);

            using var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
            if ((res.StatusCode == HttpStatusCode.TooManyRequests ||
                 res.StatusCode == HttpStatusCode.ServiceUnavailable) && attempt < retries)
            {
                var wait = res.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(1);
                await Task.Delay(wait);
                continue;
            }
            if (!res.IsSuccessStatusCode)
                throw new HttpRequestException($"HTTP {(int)res.StatusCode}: {await res.Content.ReadAsStringAsync()}");

            await using (var file = File.Create(path))
                await res.Content.CopyToAsync(file);
            return int.Parse(res.Headers.GetValues("X-Total-Count").First());
        }
    }
}

// var count = await LabelApi.ZplToPdfAsync(File.ReadAllText("label.zpl"), "label.pdf");
```

The label in this example:

```zpl
^XA
^FO30,30^GB752,70,70^FS
^FO50,48^A0N,40,40^FR^FDRETURN LABEL^FS
^FO30,120^A0N,26,26^FDRMA 77 2026 0193^FS
^FO30,155^A0N,26,26^FDReason: wrong size^FS
^BY2,3,100
^FO30,200^BCN,100,Y,N,N^FDRMA7720260193^FS
^XZ
```

## Notes for .NET

- In ASP.NET Core, register the client with IHttpClientFactory (services.AddHttpClient) instead of a static field; the request code stays the same.
- Retry-After arrives as a number of seconds, which .NET exposes as RetryAfter.Delta. The daily limit sets it to the seconds left until midnight UTC: cap the delay in a background job.
- The API reads the body as text. StringContent with UTF-8 keeps accents intact if the label starts with ^CI28.
- To return the PDF from a controller, stream it with File(stream, "application/pdf") rather than loading it into a byte array.

## Errors

| 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 | The body is over 1 MB, or the ZPL produces more than 50 labels. | 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. |

## Limits

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

**Is there a .NET library to convert ZPL to PDF?** You can use one, but the API needs nothing beyond HttpClient: it returns the finished PDF. There is no native renderer to install or license.

**Can I print the PDF from C# afterwards?** Yes, with any PDF printing component, on any printer. To print ZPL on a Zebra printer you would send the ZPL itself, not the PDF.
