Design
TXT templates
Write a plain text template, add Carbone tags, and generate text files from JSON data, like SMS, receipts, bank files and ZPL labels.
COMMUNITY FEATURE
Available for:
Carbone Cloud
Carbone On-premise
Embedded Carbone JS
v5.0+
Any plain text file can be a template. Write Carbone tags in it, send your JSON data to the API, and get back a text file with the same characters, spaces and line breaks. Use it for messages, printer code and files read by other systems.
Basic
Write a tag where a value goes. In {d.customer.firstName}, d is the root of the data and customer.firstName is the path to the value. The rest of the file is printed as is, like the text of this SMS.
{
"carrier": "Acme Logistics",
"customer": { "firstName": "Jane" },
"parcel": {
"tracking": "AL7829341056",
"deliveryDate": "2026-11-18",
"slot": "morning"
},
"trackingUrl": "https://track.acme.example/AL7829341056"
}Hi {d.customer.firstName}, your parcel {d.parcel.tracking} arrives on {d.parcel.deliveryDate:formatD('dddd, MMMM D')} ({d.parcel.slot}).
Track or reschedule: {d.trackingUrl}
{d.carrier}Hi Jane, your parcel AL7829341056 arrives on Wednesday, November 18 (morning).
Track or reschedule: https://track.acme.example/AL7829341056
Acme Logistics- Formatters change how a value is printed:
:formatD('dddd, MMMM D')writes the delivery date in words. See the date formatters. - A line break in a value is printed as a line break, without
:convCRLF.
Learn more about tags.
Loops
A loop repeats the text from its [i] tag to its [i+1] tag, for each item of an array:
- The line to repeat starts with the
[i]tag:{d.items[i].name}. - The line after the loop starts with the
[i+1]tag:{d.items[i+1]}. The tag is removed and the rest of its line is printed once.
In this receipt, :padr and :padl add spaces to align the columns, :substr(0, 24) cuts the long names, and :aggSum computes the subtotal.
{
"receipt": { "id": "R-104582", "date": "2026-11-09", "cashier": "Maria" },
"items": [
{ "name": "Hex bolts M8x40, box of 50", "qty": 2, "price": 12.49 },
{ "name": "Safety glasses, clear", "qty": 1, "price": 8.99 },
{ "name": "Nitrile gloves L, 100 pcs", "qty": 3, "price": 14.5 },
{ "name": "Measuring tape 25 ft", "qty": 1, "price": 19.95 }
],
"taxRate": 0.0725,
"payment": { "card": "VISA", "last4": "4242" }
} ACME HARDWARE
1200 Industrial Pkwy, Columbus OH
(614) 555-0142
Receipt {d.receipt.id:padr(14)}{d.receipt.date:formatD('ll'):padl(20)}
Cashier: {d.receipt.cashier}
------------------------------------------
ITEM QTY AMOUNT
{d.items[i].name:substr(0, 24):padr(24)}{d.items[i].qty:padl(5)}{d.items[i].qty:mul(.price):formatN(2):padl(13)}
{d.items[i+1]}------------------------------------------
{d.items[].qty:mul(.price):aggSum:set(c.subtotal)}SUBTOTAL{c.subtotal:formatN(2):padl(34)}
TAX{c.subtotal:mul(d.taxRate):formatN(2):padl(39)}
TOTAL{c.subtotal:mul(d.taxRate):add(c.subtotal):formatN(2):padl(37)}
Paid by {d.payment.card} ending in {d.payment.last4}
Thank you for shopping! ACME HARDWARE
1200 Industrial Pkwy, Columbus OH
(614) 555-0142
Receipt R-104582 Nov 9, 2026
Cashier: Maria
------------------------------------------
ITEM QTY AMOUNT
Hex bolts M8x40, box of 2 24.98
Safety glasses, clear 1 8.99
Nitrile gloves L, 100 pc 3 43.50
Measuring tape 25 ft 1 19.95
------------------------------------------
SUBTOTAL 97.42
TAX 7.06
TOTAL 104.48
Paid by VISA ending in 4242
Thank you for shopping!- Text before the
[i]tag is printed once, before the first item. To repeat a prefix, write an empty[i]tag before it:{d.items[i]}- {d.items[i].name}. :setstores the subtotal for the tax and the total.
Learn more about loops, aggregators and :set.
Conditions
Conditions print a value or remove lines, depending on the data:
- Inline conditions print one value or another:
{d.order.paid:ifEQ(true):show('paid'):elseShow('pay at pickup')}. - Conditional blocks remove lines:
:hideBeginand:hideEndremove the lines between them when the condition is true,:showBeginand:showEndwhen it is false. Write the begin tag at the end of the line above the block, and the end tag at the end of the last line of the block.
This order confirmation is the plain-text part of an email. The discount line is removed because the discount is 0.
{
"customer": { "firstName": "Jane" },
"order": {
"id": "SO-58213",
"subtotal": 97.42,
"discount": 0,
"total": 97.42,
"paid": false,
"pickup": true,
"readyDate": "2026-11-12",
"giftMessage": "Happy birthday, Tom!"
},
"store": { "name": "Acme Hardware Columbus", "address": "1200 Industrial Pkwy, Columbus OH" }
}Subject: Order {d.order.id} confirmed
Hi {d.customer.firstName},
Thanks for your order {d.order.id}.
Subtotal: {d.order.subtotal:formatC}{d.order.discount:ifEQ(0):hideBegin}
Discount: -{d.order.discount:formatC}{d.order.discount:hideEnd}
Total: {d.order.total:formatC} ({d.order.paid:ifEQ(true):show('paid'):elseShow('pay at pickup')}){d.order.pickup:ifEQ(false):hideBegin}
Pick it up at {d.store.name}, {d.store.address},
from {d.order.readyDate:formatD('dddd, MMMM D')}. Bring this email.{d.order.pickup:hideEnd}{d.order.giftMessage:ifEM:hideBegin}
Gift message: "{d.order.giftMessage}"{d.order.giftMessage:hideEnd}
Questions? Reply to this email.
Acme HardwareSubject: Order SO-58213 confirmed
Hi Jane,
Thanks for your order SO-58213.
Subtotal: $97.42
Total: $97.42 (pay at pickup)
Pick it up at Acme Hardware Columbus, 1200 Industrial Pkwy, Columbus OH,
from Thursday, November 12. Bring this email.
Gift message: "Happy birthday, Tom!"
Questions? Reply to this email.
Acme HardwareThe tags can also start the lines, like loops: the begin tag at the start of the first line of the block, and the end tag at the start of the line after it. Both placements remove the block without leaving an empty line. Learn more about inline conditions and conditional blocks.
Fixed-width files
Banks, payroll providers and legacy systems often read fixed-width files: each field starts at a set position and has a set length. Build the fields with formatters:
:padr(25)adds spaces after a text up to 25 characters, and:padl(12, '0')adds zeros before a number.:substr(0, 25)cuts a longer text.:padland:padrnever cut a value: write:substrbefore them.:mul(100):round(0)turns an amount into cents.
This payment file has a header record H, one detail record D per payment, and a trailer record T with the number of payments and their total. Each record is 64 characters long.
{
"company": { "id": "ACME000142", "name": "Acme Corporation" },
"batch": { "date": "2026-11-12" },
"payments": [
{ "account": "4471209386", "name": "Müller Präzisionsteile GmbH", "amount": 12480, "reference": "INV-2041" },
{ "account": "88120034", "name": "Café Lumière", "amount": 356.9, "reference": "INV-2042" },
{ "account": "5500123497", "name": "Northwind Industrial Supply Co.", "amount": 7015.25, "reference": "INV-2043" }
]
}H{d.batch.date:formatD('YYYYMMDD')}{d.company.id:padr(10)}{d.company.name:upperCase:substr(0, 45):padr(45)}
{d.payments[i]}D{d.payments[i].account:padl(18, '0')}{d.payments[i].name:unaccent:upperCase:substr(0, 25):padr(25)}{d.payments[i].amount:mul(100):round(0):padl(12, '0')}{d.payments[i].reference:substr(0, 8):padr(8)}
{d.payments[i+1]}T{d.payments:len:padl(6, '0')}{d.payments[].amount:aggSum:mul(100):round(0):padl(15, '0'):padr(57)}H20261112ACME000142ACME CORPORATION
D000000004471209386MULLER PRAZISIONSTEILE GM000001248000INV-2041
D000000000088120034CAFE LUMIERE 000000035690INV-2042
D000000005500123497NORTHWIND INDUSTRIAL SUPP000000701525INV-2043
T000003000000001985215 - The detail line starts with an empty
{d.payments[i]}tag, so the record typeDis repeated for each payment. :unaccent:upperCasewrites the names in capital letters without accents, for systems that only read ASCII.:padr(57)after the total fills the trailer with spaces up to 64 characters.- The template uses Windows line endings (CRLF), so the report does too.
Learn more about :padl, :padr and :substr.
ZPL labels
Zebra label printers read ZPL, a text language: ^FO places a field, ^FD holds its text and ^BC draws a Code 128 barcode. Write the ZPL of your label with Carbone tags in the ^FD fields, then send the report to the printer. A loop around ^XA and ^XZ prints one shipping label per package.
{
"orderId": "SO-58213",
"service": "GROUND",
"shipper": { "name": "Acme Hardware DC", "street": "4500 Logistics Way", "city": "Columbus, OH 43217" },
"recipient": {
"name": "Ana Sánchez",
"company": "Northwind Industrial Supply",
"street": "780 Harbor Blvd",
"city": "Long Beach, CA 90802"
},
"packages": [
{ "number": 1, "weight": 12.4, "tracking": "1AC2026111200001" },
{ "number": 2, "weight": 8.75, "tracking": "1AC2026111200002" }
]
}{d.packages[i]}^XA
^CI28
^PW812^LL1218
^FO40,40^A0N,28,28^FDFROM: {d.shipper.name}^FS
^FO40,75^A0N,28,28^FD{d.shipper.street}, {d.shipper.city}^FS
^FO40,130^GB732,3,3^FS
^FO40,160^A0N,30,30^FDSHIP TO:^FS
^FO40,205^A0N,45,45^FD{d.recipient.name}^FS
^FO40,260^A0N,45,45^FD{d.recipient.company}^FS
^FO40,315^A0N,45,45^FD{d.recipient.street}^FS
^FO40,370^A0N,45,45^FD{d.recipient.city}^FS
^FO40,450^GB732,3,3^FS
^FO40,480^A0N,70,70^FD{d.service}^FS
^FO500,495^A0N,40,40^FDPKG {d.packages[i].number} OF {d.packages:len}^FS
^FO80,600^BY3^BCN,220,Y,N,N^FD{d.packages[i].tracking}^FS
^FO40,920^A0N,30,30^FDORDER {d.orderId} WEIGHT {d.packages[i].weight:formatN(1)} LB^FS
^XZ
{d.packages[i+1]}^XA
^CI28
^PW812^LL1218
^FO40,40^A0N,28,28^FDFROM: Acme Hardware DC^FS
^FO40,75^A0N,28,28^FD4500 Logistics Way, Columbus, OH 43217^FS
^FO40,130^GB732,3,3^FS
^FO40,160^A0N,30,30^FDSHIP TO:^FS
^FO40,205^A0N,45,45^FDAna Sánchez^FS
^FO40,260^A0N,45,45^FDNorthwind Industrial Supply^FS
^FO40,315^A0N,45,45^FD780 Harbor Blvd^FS
^FO40,370^A0N,45,45^FDLong Beach, CA 90802^FS
^FO40,450^GB732,3,3^FS
^FO40,480^A0N,70,70^FDGROUND^FS
^FO500,495^A0N,40,40^FDPKG 1 OF 2^FS
^FO80,600^BY3^BCN,220,Y,N,N^FD1AC2026111200001^FS
^FO40,920^A0N,30,30^FDORDER SO-58213 WEIGHT 12.4 LB^FS
^XZ
^XA
^CI28
^PW812^LL1218
^FO40,40^A0N,28,28^FDFROM: Acme Hardware DC^FS
^FO40,75^A0N,28,28^FD4500 Logistics Way, Columbus, OH 43217^FS
^FO40,130^GB732,3,3^FS
^FO40,160^A0N,30,30^FDSHIP TO:^FS
^FO40,205^A0N,45,45^FDAna Sánchez^FS
^FO40,260^A0N,45,45^FDNorthwind Industrial Supply^FS
^FO40,315^A0N,45,45^FD780 Harbor Blvd^FS
^FO40,370^A0N,45,45^FDLong Beach, CA 90802^FS
^FO40,450^GB732,3,3^FS
^FO40,480^A0N,70,70^FDGROUND^FS
^FO500,495^A0N,40,40^FDPKG 2 OF 2^FS
^FO80,600^BY3^BCN,220,Y,N,N^FD1AC2026111200002^FS
^FO40,920^A0N,30,30^FDORDER SO-58213 WEIGHT 8.8 LB^FS
^XZ^CI28tells the printer that the text is UTF-8, the encoding of Carbone reports, so accented names print correctly.^and~start ZPL commands, so a value that contains them breaks the label. Add^FHbefore^FDand encode these characters in hexadecimal:
^FH^FD{d.recipient.company:replace('_', '_5F'):replace('^', '_5E'):replace('~', '_7E')}^FS
Translations
Write static text in {t( )} tags, and add the :t formatter to translate values from the data, like the delivery slot. Carbone uses the translations for the lang of the report. This example uses "lang": "fr-fr" and the dictionary in the Translations tab. Dates follow the language too: :formatD('dddd LL') prints the date in French.
{
"carrier": "Acme Logistics",
"customer": { "firstName": "Jane" },
"parcel": {
"tracking": "AL7829341056",
"deliveryDate": "2026-11-18",
"slot": "morning"
},
"trackingUrl": "https://track.acme.example/AL7829341056"
}{
"fr-fr": {
"Hi": "Bonjour",
"your parcel": "votre colis",
"arrives on": "arrive le",
"morning": "matin",
"afternoon": "après-midi",
"Track or reschedule:": "Suivre ou reprogrammer :"
}
}{t(Hi)} {d.customer.firstName}, {t(your parcel)} {d.parcel.tracking} {t(arrives on)} {d.parcel.deliveryDate:formatD('dddd LL')} ({d.parcel.slot:t}).
{t(Track or reschedule:)} {d.trackingUrl}
{d.carrier}Bonjour Jane, votre colis AL7829341056 arrive le mercredi 18 novembre 2026 (matin).
Suivre ou reprogrammer : https://track.acme.example/AL7829341056
Acme LogisticsPut the punctuation in the translation when it changes with the language, like the space before a colon in French: {t(Track or reschedule:)}. Learn more about translations.
Best practices
- Encoding: save the template in UTF-8, the encoding of the report. For a system that only reads ASCII, or counts field widths in bytes, remove the accents with
:unaccent. - Fixed widths: write
:ifEM:show('')before:padr, so an empty value still fills its field:{d.reference:ifEM:show(''):padr(8)}. Remove the line breaks from your data: a line break splits a record. - Tags that print nothing: an alias or a
:settag alone on its line leaves an empty line. Write it at the start of another line, like the subtotal of the receipt. - Printer codes: write control codes, like the paper cut command of a receipt printer, in the template. Carbone removes them from the data, except tabs and line breaks.
Automate a TXT file
To generate a text file from a TXT template with the Carbone API:
- Prepare two files: a TXT template with Carbone tags, like
{d.name}, and the JSON data to populate it. You can use the files of the Fixed-width files example. - Generate the file in one of three ways:
- Use an official Carbone integration, like N8N, Make, Zapier, Airtable or Bubble.
- Use a Carbone SDK: Node.js, Go, Python, Java, JavaScript or PHP.
- Send a
POST /render/templateHTTP request to the Carbone API, like this curl command:
curl --location --request POST 'https://api.carbone.io/render/template?download=true' \
--header 'carbone-version: 5' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer API_TOKEN' \
--data-raw "{
\"data\": $(cat fixed-width-data.json),
\"template\": \"$(base64 -i fixed-width-template.txt)\",
\"lang\": \"en-us\"
}" \
--output payments.txt
💡 Linux users: replacebase64 -i fixed-width-template.txtwithbase64 -w 0 fixed-width-template.txt
Key parameters
| Parameter | Description |
|---|---|
template |
Your TXT template, encoded as a Base64 string. |
data |
JSON data used to populate the template. |
lang |
Language of the report, used by the formatters and translations: "en-us" prints {d.date:formatD('ll')} as Nov 9, 2026. |
convertTo |
Optional: "txt" by default. "pdf", "docx", "odt" or "html" print the text in a monospace font. |
?download=true |
Query parameter to download the document as a stream. |
Authorization |
Header required for the Carbone Cloud API. Get your API key here. |
For more details, refer to the Carbone API Documentation.