Jinja2PDF
Shipping label, 100×150 mm
A custom paper size for thermal printers, a Code 128 tracking barcode, a QR code built from a URL and the tracking number, and the package contents summed in the template. barcode also does EAN-13, ITF-14, DataMatrix and PDF417.
Free plan, no card. You get your own copy in the editor, published and ready for the API.
Rendered result
What this example shows
- The paper is a custom size,
100mm×150mmwith 4 mm margins, the common format of thermal label printers. barcode(shipment.tracking, { type: 'code128', height: 18 })draws the tracking code; the same helper does EAN-13, ITF-14, DataMatrix and PDF417.- The QR code is built in the template:
~joins the tracking URL and the number beforeqrcodeencodes it. sum('qty')counts the items, so the label says “3 items” without the caller adding anything up.- The PDF is exactly one label in size, so your print service can send it to the label printer as it is: no browser and no print dialogue in between.
Helpers used
Template folder
<section class="label">
<header>
<strong>{{ shipment.service | upper }}</strong>
<span>{{ shipment.weight_kg | number(1) }} kg</span>
</header>
<p class="from">
{{ shipment.from.name }}, {{ shipment.from.street }}, {{ shipment.from.city }}, {{ shipment.from.country }}
</p>
<address class="to">
<strong>{{ shipment.to.name }}</strong><br>
{{ shipment.to.street }}<br>
{{ shipment.to.city }}<br>
{{ shipment.to.country }}
</address>
<img class="tracking" src="{{ barcode(shipment.tracking, { type: 'code128', height: 18 }) }}" alt="Tracking barcode">
<footer>
<img src="{{ qrcode('https://track.example/' ~ shipment.tracking, { size: 88 }) }}" alt="Tracking link">
<ul>
{% for item in shipment.items %}
<li>{{ item.qty }} × {{ item.sku }}</li>
{% endfor %}
<li>{{ shipment.items | sum('qty') }} items · {{ shipment.id }}</li>
</ul>
</footer>
</section>body { font-family: 'Noto Sans', sans-serif; font-size: 9pt; color: #000; }
.label {
height: 141mm;
box-sizing: border-box;
display: flex;
flex-direction: column;
border: 1.5px solid #000;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 2mm 3mm;
background: #000;
color: #fff;
font-size: 15pt;
}
.from { margin: 0; padding: 2mm 3mm; border-bottom: 1px solid #000; font-size: 7pt; }
.to { flex: 1; padding: 5mm 3mm; font-style: normal; font-size: 13pt; line-height: 1.35; }
.tracking { display: block; width: 88mm; margin: 0 auto 3mm; }
footer { display: flex; align-items: center; gap: 4mm; padding: 2mm 3mm; border-top: 1px solid #000; }
footer ul { margin: 0; padding: 0; list-style: none; font-size: 8pt; line-height: 1.5; }{
"pdf": {
"metadata": {
"title": "Shipping label"
}
},
"paper": {
"width": "100mm",
"height": "150mm"
},
"margin": {
"top": "4mm",
"right": "4mm",
"bottom": "4mm",
"left": "4mm"
},
"locale": "en-GB"
}{
"shipment": {
"id": "SHP-2026-118204",
"service": "Express",
"tracking": "00000000000000000000",
"weight_kg": 2.4,
"from": {
"name": "Fennlor Studio GmbH",
"street": "Musterstraße 1",
"city": "12345 Musterstadt",
"country": "DE"
},
"to": {
"name": "Erika Mustermann",
"street": "Beispielweg 2",
"city": "54321 Beispielstadt",
"country": "Germany"
},
"items": [
{
"sku": "FF-1001",
"qty": 2
},
{
"sku": "FF-2040",
"qty": 1
}
]
}
}Render it through the API
Save the sample data as data.json and send it with the template’s name. TypeScript and Python use the SDKs; every other language posts the same JSON to the REST API with its standard library. Renders with a key that starts with ff_test_ are not metered; on the Free plan they carry a small watermark.
jq '{template: "shipping-label", data: ., filename: "label-SHP-2026-118204.pdf"}' data.json \
| curl https://api-eu.formfeed.dev/v1/renders \
-H "Authorization: Bearer ff_test_..." \
-H "Content-Type: application/json" \
--data-binary @-import { Formfeed } from '@formfeed/sdk';
import data from './data.json' with { type: 'json' };
const formfeed = new Formfeed({ apiKey: process.env.FORMFEED_API_KEY! });
const render = await formfeed.renders.create({
template: 'shipping-label',
data,
filename: 'label-SHP-2026-118204.pdf',
});
console.log(render.status, render.download_url);import json
import os
from formfeed import Formfeed
formfeed = Formfeed(api_key=os.environ["FORMFEED_API_KEY"])
with open("data.json", encoding="utf-8") as file:
data = json.load(file)
render = formfeed.renders.create(
template="shipping-label",
data=data,
filename="label-SHP-2026-118204.pdf",
)
print(render.status, render.download_url)<?php
$body = [
'template' => 'shipping-label',
'data' => json_decode(file_get_contents('data.json'), true),
'filename' => 'label-SHP-2026-118204.pdf',
];
$curl = curl_init('https://api-eu.formfeed.dev/v1/renders');
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('FORMFEED_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($body),
]);
$render = json_decode(curl_exec($curl), true);
echo $render['status'], ' ', $render['download_url'], PHP_EOL;package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
data, err := os.ReadFile("data.json")
if err != nil {
panic(err)
}
body, _ := json.Marshal(map[string]any{
"template": "shipping-label",
"data": json.RawMessage(data),
"filename": "label-SHP-2026-118204.pdf",
})
request, _ := http.NewRequest("POST", "https://api-eu.formfeed.dev/v1/renders", bytes.NewReader(body))
request.Header.Set("Authorization", "Bearer "+os.Getenv("FORMFEED_API_KEY"))
request.Header.Set("Content-Type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil {
panic(err)
}
defer response.Body.Close()
var render struct {
Status string `json:"status"`
DownloadURL string `json:"download_url"`
}
json.NewDecoder(response.Body).Decode(&render)
fmt.Println(render.Status, render.DownloadURL)
}using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
var body = new
{
template = "shipping-label",
data = JsonDocument.Parse(File.ReadAllText("data.json")).RootElement,
filename = "label-SHP-2026-118204.pdf",
};
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("FORMFEED_API_KEY"));
var response = await http.PostAsJsonAsync("https://api-eu.formfeed.dev/v1/renders", body);
var render = await response.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine($"{render.GetProperty("status")} {render.GetProperty("download_url")}");import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
public class Render {
public static void main(String[] args) throws Exception {
String data = Files.readString(Path.of("data.json"));
String body = """
{
"template": "shipping-label",
"filename": "label-SHP-2026-118204.pdf",
"data": %s
}
""".formatted(data);
HttpRequest request = HttpRequest.newBuilder(URI.create("https://api-eu.formfeed.dev/v1/renders"))
.header("Authorization", "Bearer " + System.getenv("FORMFEED_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body()); // status, download_url, page_count, units …
}
}require "json"
require "net/http"
body = {
template: "shipping-label",
data: JSON.parse(File.read("data.json")),
filename: "label-SHP-2026-118204.pdf",
}
response = Net::HTTP.post(
URI("https://api-eu.formfeed.dev/v1/renders"),
JSON.generate(body),
"Authorization" => "Bearer #{ENV.fetch("FORMFEED_API_KEY")}",
"Content-Type" => "application/json"
)
render = JSON.parse(response.body)
puts render["status"], render["download_url"]Start rendering today
The free plan includes 100 units a month and free test renders. Change plans when you need more.
