Design

HTML templates

Write a template in HTML and CSS, add Carbone tags, and generate it from JSON data as a PDF or an image.
COMMUNITY FEATURE Available for:
Carbone Cloud
Carbone On-premise
Embedded Carbone JS
  v5.0+ 

Any HTML file can be a template. Write Carbone tags in the markup, send your JSON data to the API, and get back a PDF or an image that looks exactly like the page in a browser. Loops, conditions and formatters run inside the template, so you need no separate templating layer.

Basic

Carbone needs two inputs to generate a document:

In {d.user.firstName}, d is the root of the JSON data and user.firstName is the path to the value. The path must match the structure of the JSON. Any valid HTML works: Carbone replaces only the tags.

data
{
  "user": {
    "firstName": "John",
    "lastName": "Doe",
    "email": "john.doe@carbone.io"
  },
  "membershipLevel": "Premium"
}
template
 <!DOCTYPE html>
 <html>
    <head>
        <title>Welcome Email </title>
    </head>
    <body>
        <h1>Welcome, {d.user.firstName} {d.user.lastName}! </h1>
        <p>Thank you for registering with us. </p>
        <p>Your email address is: {d.user.email} </p>
        <p>Your membership level: {d.membershipLevel} </p>
    </body>
 </html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
    <head>
        <title>Welcome Email</title>
    </head>
    <body>
        <h1>Welcome, John Doe!</h1>
        <p>Thank you for registering with us.</p>
        <p>Your email address is: john.doe@carbone.io</p>
        <p>Your membership level: Premium </p>
    </body>
</html>

Loops

A loop repeats a part of the template for each item of an array, like the rows of a table, the items of a list or a series of images. A loop needs two tags:

Carbone repeats everything between the two tags: text, tags, HTML and styles.

data
{
  "users": [
    { "id": 1, "name": "Alice", "role": "Admin" },
    { "id": 2, "name": "Bob", "role": "Editor" },
    { "id": 3, "name": "Charlie", "role": "Viewer" }
  ]
}
template
<!DOCTYPE html>
<html>
<body>
    <h1>User List</h1>
    <table>
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th>Role</th>
            </tr>
        </thead>
        <tbody>
            <!-- Start of loop -->
            <tr>
                <td>{d.users[i].id}</td>
                <td>{d.users[i].name}</td>
                <td>{d.users[i].role}</td>
            </tr>
            <!-- End of loop -->
            <tr>
                <td>{d.users[i+1]}</td>
            </tr>
        </tbody>
    </table>
</body>
</html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
<body>
    <h1>User List</h1>
    <table>
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th>Role</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>1</td>
                <td>Alice</td>
                <td>Admin</td>
            </tr>
            <tr>
                <td>2</td>
                <td>Bob</td>
                <td>Editor</td>
            </tr>
            <tr>
                <td>3</td>
                <td>Charlie</td>
                <td>Viewer</td>
            </tr>
        </tbody>
    </table>
</body>
</html>

Carbone loops need no keyword like for or foreach. The [i] and [i+1] tags define the loop.

Conditions

Show, hide or style the elements of the template based on the data, in one of three ways:

Inline HTML/CSS Conditions

Write a tag in a style attribute, or anywhere in the markup, to apply styles or HTML from the data. Add an inline condition to the tag to show or hide an element, or to change its style:

Inject CSS Values Example

In the following example, the display, visibility, and opacity properties are conditionally set using Carbone tags:

data
{
    "display": "none",
    "showDiv": false,
    "showParagraph": false,
    "showSpan": false
}
template
<!DOCTYPE html>
<html>
    <body>
        <!-- Inject CSS values -->
        <div style="display:{d.display};">This element is hidden and takes no space, only if 'display' is 'none'.</div>
<!-- Inject CSS values based on conditions --> <div style="display:{d.showDiv:ifEQ(true):show('block'):elseShow('none')};">This element is hidden and takes no space, only if 'showDiv' is 'false'.</div> <p style="visibility:{d.showParagraph:ifEQ(true):show('visible'):elseShow('hidden')};"> This paragraph is hidden but occupies space, only if 'showParagraph' is 'false'.</p> <span style="opacity:{d.showSpan:ifEQ(true):show('1'):elseShow('0')};">This element is invisible but still clickable, only if the 'showSpan' is 'false'.</span> </body> </html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
    <body>
        <!-- Inject CSS values -->
        <div style="display:none;">This element is hidden and takes no space, only if 'display' is 'none'.</div>
<!-- Inject CSS values based on conditions --> <div style="display:none;">This element is hidden and takes no space, only if 'showDiv' is 'false'.</div> <p style="visibility:hidden;"> This paragraph is hidden but occupies space, only if 'showParagraph' is 'false'.</p> <span style="opacity:0;">This element is invisible but still clickable, only if the 'showSpan' is 'false'.</span> </body> </html>

Inject CSS Properties Example

This example demonstrates how to dynamically inject entire CSS properties into an HTML element using Carbone tags. Two methods are available:

data
{
    "showImage": true,
    "imageStyle": "display: none;"
}
template
<!DOCTYPE html>
<html>
    <body>
        <!-- Inject CSS properties -->
        <img src="image.jpg" style="width: 150px;{d.imageStyle}border-radius: 8px;" alt="Hidden Image">
<!-- Inject CSS properties based on a condition --> <img src="image.jpg" style="{d.showImage:ifEQ(true):show('display:block;'):elseShow(d.imageStyle)}" alt="Visible Image"> </body> </html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
    <body>
        <!-- Inject CSS properties -->
        <img src="image.jpg" style="width: 150px;display: none;border-radius: 8px;" alt="Hidden Image">
<!-- Inject CSS properties based on a condition --> <img src="image.jpg" style="display:block;" alt="Visible Image"> </body> </html>

Inject Style Attribute Example

This example shows how to inject an entire style attribute from your dataset into an HTML element.

data
{
    "buttonTheme": "ocean",
    "buttonStyles1": "style="background-color: green;color: white;border: none;"",
    "buttonStyles2": "style="background-color: black;color: red;border: 1px solid white;""
}
template
<!DOCTYPE html>
<html>
    <body>
        <!-- 3. Inject CSS Style Attribute -->
        <button {d.buttonStyles1}>Click Me</button>
        <!-- Inject CSS Style Attribute based on a condition -->
        <button {d.buttonTheme:ifEQ('forest'):show(d.buttonStyles1):elseShow(d.buttonStyles2)}>Click Me</button>
    </body>
</html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
    <body>
        <!-- 3. Inject CSS Style Attribute -->
        <button style="background-color: green;color: white;border: none;">Click Me</button>
        <!-- Inject CSS Style Attribute based on a condition -->
        <button style="background-color: black;color: red;border: 1px solid white;">Click Me</button>
    </body>
</html>

Inject HTML Block

This example demonstrates how to inject raw HTML blocks from your dataset into your template using conditions and the :html formatter.

data
{
    "displayNote": false,
    "noteSection": "<div style='border-left: 3px solid #ff9800; padding: 8px;'><strong>Note:</strong> Maintenance at 2 AM UTC.</div>",
    "nothingSection": "<strong style='text-align: center; padding: 10px; background: #f0f8ff;'>Nothing Significant To Report.</strong>"
}
template
<!DOCTYPE html>
<html>
    <body>
        <!-- Inject HTML Block -->
        {d.noteSection:html}
<!-- Inject HTML Block based on a condition --> {d.displayNote:ifEQ(true):show(d.noteSection):elseShow(d.nothingSection):html} </body> </html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
    <body>
        <!-- Inject HTML Block -->
        <div style='border-left: 3px solid #ff9800; padding: 8px;'><strong>Note:</strong> Maintenance at 2 AM UTC.</div>
<!-- Inject HTML Block based on a condition --> <strong style='text-align: center; padding: 10px; background: #f0f8ff;'>Nothing Significant To Report.</strong> </body> </html>

Drop / Keep

:drop() and :keep() remove or keep the element that contains their tag, like a table, a row or a paragraph, based on a condition:

Element Description Tag position Example
p Drop/keep a paragraph Within a paragraph {d.text:ifEM:drop(p)}
row Drop/keep a table row In a table row {d.condition:ifNE(value):keep(row)}
col Drop/keep an entire table column In any cell of the target column {d.condition:ifEQ(value):drop(col)}
table Drop/keep an entire table Inside the table {d.condition:ifEQ(value):drop(table)}
div Drop/keep a div block Within a div element {d.condition:ifEQ(value):drop(div)}
span Drop/keep a span element Within a span element {d.condition:ifEQ(value):drop(span)}

Drop a Table Example

data
{
  "showTable": false,
  "list": [
    {
      "name": "Model 5"
    },
    {
      "name": "Model 3"
    },
    {
      "name": "Falcon Heavy"
    }
  ]
}
template
<!DOCTYPE html>
<html>
    <body>
        <table>{d.showTable:ifEQ(false):drop(table)}
            <tr><td>{d.list[i].name}</td></tr>
            <tr><td>{d.list[i+1].name}</td></tr>
        </table>
    </body>
</html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
    <body>
    </body>
</html>

Drop a Table Row Example

data
[
  { "name": "Falcon 9" },
  { "name": "Model 5" },
  { "name": "Model 3" },
  { "name": "Falcon Heavy" }
]
template
<!DOCTYPE html>
<html>
    <body>
        <table>
            <tr><td>{d[i].name}{d[i].name:ifIN('Falcon'):drop(row)}</td></tr>
            <tr><td>{d[i+1].name}</td></tr>
        </table>
    </body>
</html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
    <body>
        <table>
            <tr><td>Model 5</td></tr>
            <tr><td>Model 3</td></tr>
        </table>
    </body>
</html>

Drop a Paragraph Example

data
{ "text": "" }
template
<!DOCTYPE html>
<html>
    <body>
        <p>{d.text}{d.text:ifEM:drop(p, 2)}This paragraph and the next one will be dropped if 'text' is empty.</p>
        <p>This paragraph will also be dropped.</p>
        <p>This paragraph will remain.</p>
    </body>
</html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
    <body>
        <p>This paragraph will remain.</p>
    </body>
</html>

Conditional Sections

:showBegin/:showEnd and :hideBegin/:hideEnd show or hide a part of the template based on a condition. Use them around several elements, like groups of paragraphs, tables or whole pages:

showBegin / showEnd Example

The details section containing a list is shown with the showBegin/showEnd formatters only if the showDetails field is true. Here showDetails is false, so the section is removed.

data
{
  "showDetails": false,
  "product": "Carbone Pro"
}
template
<!DOCTYPE html>
<html>
    <body>
        <p>Product: {d.product}</p>
        {d.showDetails:ifEQ(true):showBegin}
        <p>Details:</p>
        <ul>
            <li>Feature 1</li>
            <li>Feature 2</li>
        </ul>
        {d.showDetails:ifEQ(true):showEnd}
        <p>Thank you for using Carbone!</p>
    </body>
</html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
    <body>
        <p>Product: Carbone Pro</p>
        <p>Thank you for using Carbone!</p>
    </body>
</html>

hideBegin / hideEnd Example

The greetings section is deleted when the field name is undefined/null/empty.

data
{
  "name": null
}
template
<!DOCTYPE html>
<html>
    <body>
        <h2> Dashboard </h2>
        {d.name:ifEM:hideBegin}
        <strong style='margin: 0; font-size: 14px;'>
            Welcome {d.name}!
        </strong>
        {d.name:ifEM:hideEnd}
    </body>
</html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
    <body>
        <h2> Dashboard </h2>
    </body>
</html>

Pictures

Write a tag in the src attribute of an <img> element to insert an image from the data. The value must be one of these:

data
{
  "userProfile": {
    "name": "John Doe",
    "profilePictureUrl": "https://carbone.io/images/john-doe.jpg",
    "profilePictureBase64": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD..."
  }
}
template
<!DOCTYPE html>
<html>
<head>
    <title>User Profile</title>
</head>
<body>
    <h1>Welcome, {d.userProfile.name}!</h1>
    <div>
        <img src="{d.userProfile.profilePictureUrl}" alt="Profile Picture" style="width:200px; height:auto;"/>
    </div>
</body>
</html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
<head>
    <title>User Profile</title>
</head>
<body>
    <h1>Welcome, John Doe!</h1>
    <div>
        <img src="data:image/jpg...IMAGE BASE 64" alt="Profile Picture" style="width:200px; height:auto;"/>
    </div>
</body>
</html>

Set the size of the image with HTML attributes or CSS:

Chromium has no network access when it converts HTML to PDF. Carbone downloads the images, stylesheets and fonts of the page itself. A resource that cannot be downloaded is not loaded. On-Premise, URLs that point to a local network are refused by default (see securityLevel).

Charts

Write a tag with the :chart formatter in the src attribute of an <img> element to insert a chart.

data
{
    "chartOptions": {
        "type": "echarts@v5a",
        "width": 600,
        "height": 400,
        "theme": null,
        "option": {
            "xAxis": {
                "type": "category",
                "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
            },
            "yAxis": {
                "type": "value"
            },
            "series": [{
                "data": [150, 230, 224, 218, 135, 147, 260],
                "type": "line"
            }]
        }
    }
}
template
<html>
    <body>
        <div class="chart-container">
            <h2 class="chart-title">Sales Overview</h2>
            <img src="{d.chartOptions:chart}" alt="Chart Image" width="600" height="400">
        </div>
    </body>
</html>
Carbone Merge Icon
result
<html>
    <body>
        <div class="chart-container">
            <h2 class="chart-title">Sales Overview</h2>
            <img src="data:image/svg+xml CHART BASE 64" alt="Chart Image" width="600" height="400">
        </div>
    </body>
</html>

In this example, the {d.chartOptions:chart} tag is replaced with an SVG image of the chart, which is dynamically generated based on the data provided in your dataset.

Colors

Write a tag in a CSS color property to set the color from the data, like the theme of a document or the color of a status. Two ways are available:

data
{
  "theme": "dark",
  "textColor": "#ffffff",
  "bgColor": "#1a1a1a",
  "alertColor": "#ff4444",
  "isUrgent": true
}
template
<!DOCTYPE html>
<html>
  <body>
    <!-- Directly inject color values -->
    <div style="color: {d.textColor}; background-color: {d.bgColor}; padding: 10px;">
      This div uses colors from the dataset.
    </div>
<!-- Conditionally apply colors --> <p style="color: {d.isUrgent:ifEQ(true):show(d.alertColor):elseShow('#333333')};"> {d.isUrgent:ifEQ(true):show('Urgent message!'):elseShow('Normal message.')} </p>
<!-- Apply colors based on a theme --> <button style=" color: {d.theme:ifEQ('dark'):show('#ffffff'):elseShow('#000000')}; background-color: {d.theme:ifEQ('dark'):show('#333333'):elseShow('#ffffff')}; border: none; padding: 8px 16px; "> {d.theme:ifEQ('dark'):show('Dark Mode'):elseShow('Light Mode')} </button> </body> </html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
  <body>
    <div style="color: #ffffff; background-color: #1a1a1a; padding: 10px;">
      This div uses colors from the dataset.
    </div>
    <p style="color: #ff4444;">
      Urgent message!
    </p>
    <button style="color: #ffffff; background-color: #333333; border: none; padding: 8px 16px;">
      Dark Mode
    </button>
  </body>
</html>

Formatters

Formatters transform a value in the template. Chain a formatter to a tag with :, like {d.value:formatterName(args)}. This section covers the formatters for dates, numbers and currencies. For the full list, see the formatters documentation.

Date Formatting

:formatD() prints a date in the format of your choice. The pattern uses Day.js tokens, like YYYY, MM, DD or dddd.

data
{
  "orderDate": "2025-03-15T14:30:00Z",
  "deliveryDate": "2025-04-01T00:00:00Z"
}
template
<!DOCTYPE html>
<html>
  <body>
    <p>Order date: {d.orderDate:formatD('DD/MM/YYYY')}</p>
    <p>Delivery date: {d.deliveryDate:formatD('dddd DD MMMM YYYY')}</p>
  </body>
</html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
  <body>
    <p>Order date: 15/03/2025</p>
    <p>Delivery date: Tuesday 01 April 2025</p>
  </body>
</html>

For all the date patterns and options, see the date formatter.

Number Formatting

:formatN() sets the number of decimals and adds thousands separators. The output follows the lang option of the request.

data
{
  "quantity": 1234567.891,
  "rating": 4.5
}
template
<!DOCTYPE html>
<html>
  <body>
    <p>Quantity: {d.quantity:formatN(2)}</p>
    <p>Rating: {d.rating:formatN(1)}</p>
  </body>
</html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
  <body>
    <p>Quantity: 1,234,567.89</p>
    <p>Rating: 4.5</p>
  </body>
</html>

For all the options, see the number formatter.

Currency Formatting

:formatC() prints an amount with its currency symbol and decimals, formatted for the lang of the report.

data
{
  "price": 4999.99,
  "discount": 250
}
template
<!DOCTYPE html>
<html>
  <body>
    <p>Price: {d.price:formatC(2, 'EUR')}</p>
    <p>Discount: {d.discount:formatC(2, 'EUR')}</p>
  </body>
</html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
  <body>
    <p>Price: 4,999.99 €</p>
    <p>Discount: 250.00 €</p>
  </body>
</html>

For all the currencies and options, see the currency formatter.

Barcodes

The :barcode formatter inserts a barcode as an SVG image:

  1. Write a tag with :barcode in the src attribute of an <img> element.
  2. Set the barcode type, like qrcode, code128 or ean13. See the full list of supported barcode types.
  3. Optional: set the size in the attributes of the <img> element or in the formatter.
data
{
  "productCode": "123456789",
  "qrData": "https://example.com/product/123456789"
}
template
<!DOCTYPE html>
<html>
  <body>
    <!-- QR Code (dimensions set via img tag) -->
    <img src="{d.qrData:barcode(qrcode)}" alt="QR Code" width="200" height="200">
<!-- EAN-13 Barcode (dimensions set via formatter) --> <img src="{d.productCode:barcode(ean13, width:40, height:20)}" alt="EAN-13 Barcode"> </body> </html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
  <body>
    <!-- QR Code -->
    <img src="data:image/svg+xml;base64,...[SVG_DATA]..." alt="QR Code" width="200" height="200">
<!-- EAN-13 Barcode --> <img src="data:image/svg+xml;base64,...[SVG_DATA]..." alt="EAN-13 Barcode"> </body> </html>

Write a tag in the href attribute of an <a> element to build the link from the data, like a personalized URL.

data
{
    "documentationUrl": "https://carbone.io/documentation/",
    "isLoggedIn": true,
    "profileUrl": "https://account.carbone.io/",
    "defaultUrl": "https://account.carbone.io/login",
    "product": {
        "name": "Carbone Cloud",
        "url": "https://carbone.io/pricing.html"
    }
}
template
<!DOCTYPE html>
<html>
  <body>
    <!-- Directly inject a URL -->
    <a href="{d.documentationUrl}">Read the Docs</a>
<!-- Conditionally set a URL --> <a href="{d.isLoggedIn:ifEQ(true):show(d.profileUrl):elseShow(d.defaultUrl)}"> {d.isLoggedIn:ifEQ(true):show('Your Profile'):elseShow('Login')} </a>
<!-- Inject a URL from a nested object --> <a href="{d.product.url}">Learn about {d.product.name}</a>
<!-- Combine static and dynamic paths --> <a href="/docs/{d.product.name:lowerCase():replace(' ', '-')}">Documentation</a> </body> </html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
  <body>
    <a href="https://carbone.io/documentation/">Read the Docs</a>
    <a href="https://account.carbone.io/">Your Profile</a>
    <a href="https://carbone.io/pricing.html">Learn about Carbone Cloud</a>
    <a href="/docs/carbone-cloud">Documentation</a>
  </body>
</html>

Inject HTML in HTML

Add the :html formatter to a tag to insert HTML from the data as markup. Without it, Carbone encodes the < and > characters as &lt; and &gt;, and the HTML is printed as text.

data
{
  "welcomeMessage": "<strong>Welcome!</strong> Here’s your dashboard.",
  "alertMessage": "<div class='alert'>⚠️ Maintenance at 2 AM UTC.</div>",
  "isAdmin": true,
  "adminPanel": "<a href='/admin'>Go to Admin Panel</a>",
  "userPanel": "<a href='/profile'>Go to Profile</a>"
}
template
<!DOCTYPE html>
<html>
  <body>
    <!-- Without :html (HTML is escaped) -->
    <p>{d.welcomeMessage}</p>
<!-- With :html (HTML is rendered) --> <p>{d.welcomeMessage:html}</p>
<!-- Conditional HTML injection --> {d.isAdmin:ifEQ(true):show(d.adminPanel):elseShow(d.userPanel):html} </body> </html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
  <body>
    <!-- Escaped HTML (displayed as text) -->
    <p>&lt;strong&gt;Welcome!&lt;/strong&gt; Here’s your dashboard.</p>
<!-- Rendered HTML --> <p><strong>Welcome!</strong> Here’s your dashboard.</p>
<!-- Conditionally injected HTML --> <a href='/admin'>Go to Admin Panel</a> </body> </html>

Page Breaks

To start a new page in the PDF, define this CSS class in the <style> element of the template:

.page-break {
  page-break-before: always;
  break-before: page;
}

Then add a <div> or a <p> with the page-break class where the new page starts:

<div>Page 1 content</div>
<p class="page-break"></p>
<div>Page 2 content</div>

Page breaks apply to PDF output only. HTML and image output ignore them.

Table of Content

A table of contents links to the sections and subsections of a long document. Build it with standard HTML elements and internal links, and fill it and the headings with Carbone tags:

  1. List the sections. Write a <ul> list. Each <li> item contains an <a> link whose href attribute points to the id of a heading.
  2. Add an id to each heading. Add an id attribute to the heading of each section and subsection, like <h1> or <h2>. The links of the table of contents point to these ids.
  3. Style the table of contents with CSS (optional).
data
{
  "sections": [
    {
      "id": 1,
      "title": "First Point Header",
      "subSections": [
        {
          "id": 11,
          "title": "First Sub Point 1",
          "content": "This is the content for the first sub-point. You can add as much detail as needed here."
        },
        {
          "id": 12,
          "title": "First Sub Point 2",
          "content": "This is the content for the second sub-point. More details or paragraphs can go here."
        }
      ]
    },
    {
      "id": 2,
      "title": "Second Point Header",
      "subSections": []
    },
    {
      "id": 3,
      "title": "Third Point Header",
      "subSections": []
    }
  ]
}
template
<!DOCTYPE html>
<html>
<head>
    <title>TOC Example</title>
    <style>
        .page-break {
            page-break-before: always;
            break-before: page;
        }
    </style>
</head>
<body>
    <div id="toc_container">
        <p class="toc_title">Contents</p>
        <ul class="toc_list">
            {d.sections[i]}
            <li>
                <a href="#section_{d.sections[i].id}">{d.sections[i].title}</a>
                <ul>
                    <li><a href="#subSection_{d.sections[i].subSections[i].id}">{d.sections[i].subSections[i].title}</a>
                    </li>
                </ul>
                {d.sections[i].subSections[i+1]}
            </li>
            {d.sections[i+1]}
        </ul>
    </div>
    <div>
        {d.sections[i]}
        <p class="page-break"></p>
        <h1 id="section_{d.sections[i].id}">{d.sections[i].title}</h1>
        <h2 id="subSection_{d.sections[i].subSections[i].id}">{d.sections[i].subSections[i].title}</h2>
        <p>{d.sections[i].subSections[i].content}</p>
        {d.sections[i+1].subSections[i+1]}
    </div>
</body>
</html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
<head>
    <title>TOC Example</title>
    <style>
        .page-break {
            page-break-before: always;
            break-before: page;
        }
    </style>
</head>
<body>
    <div id="toc_container">
        <p class="toc_title">Contents</p>
        <ul class="toc_list">
            <li>
                <a href="#section_1">First Point Header</a>
                <ul>
                    <li><a href="#subSection_11">First Sub Point 1</a>
                    </li>
                </ul>
                <ul>
                    <li><a href="#subSection_12">First Sub Point 2</a>
                    </li>
                </ul>
            </li>
            <li>
                <a href="#section_2">Second Point Header</a>
            </li>
            <li>
                <a href="#section_3">Third Point Header</a>
            </li>
        </ul>
    </div>
    <div>
        <p class="page-break"></p>
        <h1 id="section_1">First Point Header</h1>
        <h2 id="subSection_11">First Sub Point 1</h2>
        <p>This is the content for the first sub-point. You can add as much detail as needed here.</p>
        <h2 id="subSection_12">First Sub Point 2</h2>
        <p>This is the content for the second sub-point. More details or paragraphs can go here.</p>
        <p class="page-break"></p>
        <h1 id="section_2">Second Point Header</h1>
        <p class="page-break"></p>
        <h1 id="section_3">Third Point Header</h1>
    </div>
</body>
</html>

Page Formats

Carbone generates PDFs in the A4 format by default. To set another standard page size, add the <carbone-pdf-options> custom element to the HTML template and set its paper-size option:

Value Description
A0, A1, A2, A3, A4, A5, A6 ISO A-series paper sizes
Letter US Letter (8.5 × 11 in)
Legal US Legal (8.5 × 14 in)
Tabloid Tabloid (11 × 17 in)
Ledger Ledger (17 × 11 in)

The PDF conversion options also set the margins, the scale, the display of headers and footers, and more.

Example

This sets the PDF to A3 size in landscape orientation.

<carbone-pdf-options paper-size="A3" landscape="true" />

Headers Footers

Add a header and a footer to every page of the PDF with two custom elements:

Headers and footers contain text, Carbone tags and page numbers:

Style them with inline CSS only: external stylesheets are not supported. Headers and footers apply to PDF output only. HTML output and image output (PNG, JPG, WebP) ignore them. They render in the PDF even when they are hidden or commented out in the HTML template.

Limitation

The header and the footer are the same on every page. You cannot turn a <carbone-pdf-header> or a <carbone-pdf-footer> on or off, or change it, on a specific page. For example, a CSS rule like .pageNumber:first-child { display: none; } does not hide the page number on the cover page: the rule is ignored, with no error. We are working on this limitation. Until then, use one of these workarounds:

  1. Split the document into two HTML templates. Put the cover page in a template without <carbone-pdf-footer>, and the body of the document in a second template with <carbone-pdf-footer>. Join them with the :appendTemplate formatter. Page numbering restarts at 1 in the second template, because the two PDFs are merged after rendering.
  2. Use a DOCX, PPTX or XLSX template. Office formats support different headers and footers on the first page, on odd and even pages, or per section. A single template can have a cover page without a footer and a footer on the other pages, with no second template to merge.

Custom Header Example

This example demonstrates a custom PDF header with dynamic content:

data
{
  "reportTitle": "Q3 Financials",
  "user": { "name": "John Doe" }
}
template
<carbone-pdf-header>
  <div style="font-size: 12px; text-align: center; width: 100%;">
    <span>Company Report | {d.reportTitle}</span>
    <span>Generated: {c.now:formatD('YYYY-MM-DD')}</span>
    <span>User: {d.user.name}</span>
  </div>
</carbone-pdf-header>
Carbone Merge Icon
result
<carbone-pdf-header>
  <div style="font-size: 12px; text-align: center; width: 100%;">
    <span>Company Report | Q3 Financials</span>
    <span>Generated: 2025-10-17</span>
    <span>User: John Doe</span>
  </div>
</carbone-pdf-header>

This example demonstrates a custom PDF footer with:

data
{
  "year": "2025",
  "company": "Acme Inc"
}
template
<carbone-pdf-footer>
  <div style="font-size: 10px; text-align: center; width: 100%;">
    Page <span class="pageNumber"></span> of <span class="totalPages"></span>
  </div>
  <div style="font-size: 9px; text-align: right;">
    © {d.year} {d.company}. All rights reserved.
  </div>
</carbone-pdf-footer>
Carbone Merge Icon
result
<carbone-pdf-footer>
  <div style="font-size: 10px; text-align: center; width: 100%;">
    Page 5 of 10
  </div>
  <div style="font-size: 9px; text-align: right;">
    © 2025 Acme Inc. All rights reserved.
  </div>
</carbone-pdf-footer>

Translations and i18n

Generate a report in any language from one HTML template. Define a translation dictionary, and set the lang option of the request. Carbone replaces the text with its translation in that language:

Formatters like :formatD(), :formatN() and :formatC() follow the language too.

data
{
  "data"         : { "date": "2025-03-15", "tool": "key1", "protection": "key2" },
  "convertTo"    : "pdf",
  "lang"         : "en-us",
  "translations" : {
    "fr-fr" : {
      "Invoice"    : "Facture",
      "Date"       : "Date",
      "key1"       : "Tournevis",
      "key2"       : "Gants"
    },
    "en-us" : {
      "Invoice"    : "Invoice",
      "Date"       : "Date",
      "key1"       : "Screwdrivers",
      "key2"       : "Gloves"
    }
  }
}
template
<!DOCTYPE html>
<html>
  <body>
    <h1>{t(Invoice)}</h1>
    <p>{t(Date)}: {d.date:formatD('DD/MM/YYYY')}</p>
    <table>
      <thead>
        <tr><th>Item</th><th>Quantity</th></tr>
      </thead>
      <tbody>
        <tr><td>{d.tool:t}</td><td>1</td></tr>
        <tr><td>{d.protection:t}</td><td>2</td></tr>
      </tbody>
    </table>
  </body>
</html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
  <body>
    <h1>Invoice</h1>
    <p>Date: 15/03/2025</p>
    <table>
      <thead>
        <tr><th>Item</th><th>Quantity</th></tr>
      </thead>
      <tbody>
        <tr><td>Screwdrivers</td><td>1</td></tr>
        <tr><td>Gloves</td><td>2</td></tr>
      </tbody>
    </table>
  </body>
</html>

Send the dictionary in the translations field of the API request body. For the full reference, with the CLI tools and the generation of dictionaries, see the translations documentation.

Comments

Carbone reads the whole template, HTML comments (<!-- -->) included. A tag inside a comment, like {d.user.name}, is replaced with its value.

data
{
  "showComment": true,
  "user": {
    "name": "John"
  },
  "comment": "<!-- Injected -->"
}
template
<!DOCTYPE html>
<html>
<head>
  <title>User Profile</title>
</head>
  <body>
      <!-- Printing Carbone Tags within Comments: -->
      <!-- {d.user.name} -->
<!-- Inject a whole comment section coming from the JSON data-set, using the :html formatter: --> {d.showComment:ifEQ(true):show(.comment):elseShow(''):html} </body> </html>
Carbone Merge Icon
result
<!DOCTYPE html>
<html>
<head>
  <title>User Profile</title>
</head>
  <body>
      <!-- Printing Carbone Tags within Comments: -->
      <!-- John -->
<!-- Inject a whole comment section coming from the JSON data-set, using the :html formatter: --> <!-- Injected --> </body> </html>

Automate an HTML file

Store the template once, then generate an HTML file for each set of data.

1. Upload the template. Carbone returns its template ID in data.id:

curl --request POST 'https://api.carbone.io/template' \
  --header 'carbone-version: 5' \
  --header 'Authorization: Bearer API_TOKEN' \
  --form 'versioning=true' \
  --form 'template=@template.html'

2. Generate the document with the template ID and the JSON data:

curl --request POST 'https://api.carbone.io/render/TEMPLATE_ID?download=true' \
  --header 'carbone-version: 5' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer API_TOKEN' \
  --data-raw "{ \"data\": $(cat data.json) }" \
  --output report.html

Without convertTo, the report has the format of the template. To get a PDF or an image, see Convert HTML to PDF. Learn more about template storage.

Convert HTML to PDF

Using Carbone with AI-generated content: AI assistants like ChatGPT, Claude and Gemini write HTML templates. Ask the assistant for a structured HTML document, add Carbone tags for the dynamic fields, like {d.clientName} or {d.date}, then call the Carbone API with your JSON data to render the PDF. It fits automated invoices, reports, contracts and certificates.

To generate a PDF from an HTML template with the Carbone API:

  1. Prepare two files: an HTML template with Carbone tags, like {d.name}, and the JSON data to populate it.
  2. Generate the PDF 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/template HTTP 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\": {},
    \"template\": \"$(base64 -i webpage.html)\",
    \"convertTo\": \"pdf\",
    \"converter\": \"C\"
  }" \
  --output result.pdf
💡 Linux users: replace base64 -i webpage.html with base64 -w 0 webpage.html

Key parameters

Parameter Description
template Your HTML template, encoded as a Base64 string.
data JSON data used to populate the template.
convertTo "pdf", or an image format. See Convert HTML to Image.
converter Required: Chromium "C". The output matches the page in Chrome.
?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.

Convert HTML to Image

Chromium renders the HTML template to a PNG, JPG/JPEG or WebP image. The request is the same as for PDF: set convertTo to the image format, or to an object with formatName and formatOptions.

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\": {},
    \"template\": \"$(base64 -i webpage.html)\",
    \"convertTo\": { \"formatName\": \"png\", \"formatOptions\": { \"PixelWidth\": 1200, \"Scale\": 2 } },
    \"converter\": \"C\"
  }" \
  --output result.png
💡 Linux users: replace base64 -i webpage.html with base64 -w 0 webpage.html

See Image options for the full rules, the limits and the error messages.

PDF Conversion Options

To customize the PDF, add the <carbone-pdf-options/> element to the HTML template and set the options as attributes. The options apply to PDF output only. Image output ignores them.

Example

This configures the PDF to use A3 size, landscape orientation, a 2.5-inch bottom margin, and disables background printing.

<carbone-pdf-options
  paper-size="A3"
  landscape="true"
  margin-bottom="2.5"
  print-background="false"
/>

Available Options

Option Type Description
landscape boolean Landscape paper orientation. Defaults to false.
display-header-footer boolean Displays the header and the footer. Defaults to false, or true when the template has a header or a footer.
print-background boolean Prints the background graphics. Defaults to true.
scale number Scale of the page rendering. Defaults to 1.
paper-size string Letter, Legal, Tabloid, Ledger, A0, A1, A2, A3, A4, A5 or A6. Defaults to A4.
paper-width number Overrides the paper width, in inches. Defaults to 8.27 (A4).
paper-height number Overrides the paper height, in inches. Defaults to 11.7 (A4).
margin-top number Overrides the top margin, in inches. Defaults to 0.5.
margin-bottom number Overrides the bottom margin, in inches. Defaults to 0.5.
margin-left number Overrides the left margin, in inches. Defaults to 0.5.
margin-right number Overrides the right margin, in inches. Defaults to 0.5.
page-ranges string Pages to print, like 1-5, 8, 11-13. Pages are printed in document order, and each page is printed once. An empty string prints the whole document. Invalid ranges are ignored. A range whose start page is greater than its end page returns an error.
prefer-css-page-size boolean Uses the page size defined in the CSS. Defaults to false: the content is scaled to fit the paper size.
generate-tagged-pdf boolean Generates a tagged PDF (PDF/UA) for accessibility, with a semantic structure for screen readers. Defaults to false.