Tutorial: Automatically generate PDF invoices with Glide and Carbone

In this guide, we’ll show you how to integrate Glide with Carbone to automatically generate invoices in PDF form.
Glide logo.

When it comes to boring, repetitive business tasks, generating PDFs is up there with some of the worst. We’re talking contracts, invoices, customer reports, SOWs, and more (snooze cruise). Good news (the poetry ends there) - there are plenty of tools out there for generating reports automatically. Today, we’re talking about Carbone - a newer player in the space, with huge potential and a ton of great features.

What is Carbone PDF Generator?

Carbone is a lightweight, open-source reporting engine that takes your existing data, drops it into a template, and turns it into a clean text file: this can be a professional PDF, Word doc, or even HTML without any extra work on your end. You set up the template and integration once, and from then on, generating reports is as simple as feeding in new data.

Unlike other tools that force you to reshape your data to fit its structure, Carbone lets you define the structure yourself. Your data stays exactly how you’ve organized it, and it’s the template that adapts - not the other way around. This makes it a lot easier to update a report when things change, without having to overhaul your entire data setup.

Carbone also plays well with other tools. It works with Make, Zapier, Retool, and more. Plus, it has an open API (and a generous free plan), so you can connect it to whatever system you’re already using.

In this guide, we’ll show you how to integrate Glide with Carbone to automate report generation, using invoices as an example. We’ll use the Glide order management app we built in one of our previous tutorials as a starting point.  But once you’ve got the basics down, you can apply the same process to any tool with an API and any document your business needs.

Two quick notes, before you begin:

  • If you’re not at all familiar with JSON, this tutorial might be a bit tricky to follow. But if you’ve at least dabbled in it and vaguely understand how to structure JSON payloads, you should be just fine.
  • You’ll need a Glide Business plan to follow this tutorial because calling external APIs from Glide is only available from the Business plan up. That said, this tutorial is really about generating Carbone reports from your existing data. If you’re using Glide with Zapier or Make, or if you’re using a different backend tool like Airtable, you can probably get away with a cheaper Glide plan while still applying everything here to work with Carbone.

Let’s get started!

First, you’ll need to get your head around the core Carbone syntax…

Quick masterclass on core Carbone syntax 

​Carbone placeholders, known as tags, are markers within your document templates that indicate where data should be inserted during report generation. These tags are enclosed in curly braces { } and follow a specific structure to bind data from your JSON dataset to the template. ​

An example document template made with Carbone.

The most common placeholders

Basic
Pulls in a single value from your data using a d. prefix: {d.fieldName}. This will be the placeholder type you’ll use most frequently in your documents.

Sub-objects (nested data)
If your data is structured into objects within objects, you can use dot notation (for example person.name) to access nested fields. This is useful when your data is grouped inside an object, like customer details inside a person object.

Arrays (lists of data)
Arrays group multiple items with the same structure—perfect for things like invoice line items, order lists, or tables. When working with arrays in Carbone, you can loop through each item using i, which represents the index of the item in the array.

Since array values are typically nested, you’ll need to combine sub-object notation with array indexing:

  • First item in the array: {d.array[i].fieldName}
  • All of the following items in the array: {d.array[i+1].fieldName}

This method allows you to dynamically reference items within an array, making it easy to structure reports with line items. In concrete terms, this means that instead of manually writing each item in an invoice, the system will automatically go through each item and insert it into the invoice.

Formatting
Carbone allows you to format various data types, including numbers, text, dates and currency. The two formatting functions you'll use the most are decimal precision and date formatting.

Decimal precision uses the :formatN formatter to control the number of decimal places in a number. For example, {d.price:formatN(2)} displays the price with two decimal places.

Date formatting uses the :formatD(patternOut, patternIn) function to format dates. The output format (patternOut) defines how the date should appear, and the input format (patternIn) is optional, defaulting to ISO 8601. For example, {d.invoiceDate:formatD(L)} formats the invoice date using the L pattern (MM/DD/YYYY). You can read more about date formats here.

Other placeholders and functions
The syntax we’ve run through is enough to get you started, but Carbone’s syntax can do a lot more. From filtering, grouping, and sorting to computations, and even charts, which is pretty wild! We’re not going deeper into that in this tutorial, but if you're curious, their documentation has all the details.

Getting started with Carbone

Setting up a Carbone account

You’ll need a Carbone account. Head over to their website and click sign up. This will create a profile and you’ll land on your account page.

Carbone account homepage.

To access a Free plan, you’ll need to add payment details. Subscribe to the Free plan and use the production API key on the My Subscription page. For this tutorial, we’ll just use the test API key instead which doesn’t require adding billing details. With this plan, you’ll be able to generate 100 documents per month and store 100 templates for free.

Preparing the PDF template

We’ve got a sample template ready to help you get started faster - just open the link and download the document. If you’d rather build your own, check out our guide on Carbone syntax above and dive into their documentation

Once you have a template, head to your Carbone account and click Open Carbone Studio in the upper right corner.

The "My Subscription" section of the Carbone account homepage.

This takes you to the Carbone Studio template setup, where you can add a new template by clicking the purple + NEW TEMPLATE button. Carbone will ask you to upload your template and give it a name. Once that’s done, hit Add.

The Carbone Studio template setup page. The dialogue box for adding a new template is open.

Now you’re in the Carbone Studio main page, where you can test and see how your data will look once you send a JSON payload through API. On the left side, you can add your JSON data. On the right side, you’ll see your invoice rendered as a preview. You can even download the rendered document in 15 different formats by clicking the Download Preview button just above the document preview.

The Carbone Studio main page. JSON data has been added on the left side and a preview of the Carbone document is on the right side.

If you’re using our template and want to test it out, just copy and paste this JSON snippet into the JSON editor on the left and see how it renders.

{
    "invoiceId": "order-4",
    "invoiceDate": "3/1/2025, 8:08:46 PM",
    "customerName": "Patricia Example",
    "street": "Street 123",
    "postalCode": "1000",
    "city": "New York",
    "country": "USA",
    "email": "patricia@example.com",
    "products": [
        {
            "name": "Wonka",
            "qty": 1,
            "unit": "Pcs",
            "price": 18,
            "discount": 0,
            "lineAmount": 18
        },
        {
            "name": "Chocolate",
            "qty": 3,
            "unit": "Box",
            "price": 60,
            "discount": 10,
            "lineAmount": 162
        },
        {
            "name": "Test Product",
            "qty": 1,
            "unit": "Pcs",
            "price": 10,
            "discount": 0,
            "lineAmount": 10
        }
    ],
    "total": 190
}

Once you’ve tested the template and everything looks good in the preview (i.e. all data from your JSON payload is rendered), it’s time to set up the source app - in our case, Glide - to make sure we’re sending the data in the right format.

Generating an invoice from Glide

In our Glide Intermediate tutorial, we built on top of our basic Glide order management app, adding a Products and Sales Lines database to support multiple products per order. In this tutorial, we’ll use that same structure to format a JSON payload that will render our invoice in Carbone. We’ll also download the invoice and attach it to our Glide order for easy access.

Preparing your line item data

In your Glide app, go to the Sales Lines database, add a new JSON Template column and name it api / json.

Under Data, in the JSON Template field, add the following JSON template:

{
      "name": $name,
      "qty": $qty,
      "unit": $unit,
      "price":$price,
      "discount": $discount,
      "lineAmount": $lineAmount
    }
ℹ️
FYI: The $ character just means it’s a placeholder - it’ll get replaced with actual data from each line item once we finalize the configuration.

Then, under Values (just below the JSON Template field), we’ll use the UI like in the image below to bind each placeholder to the right field:

  • $name → The field that holds the product name
  • $qty → The field that holds the quantity of the product
  • $unit → The field that holds the unit (like pcs)
  • $discount → The field that holds the discount amount
  • $lineAmount → The field that holds the line amount
  • $price → The field that holds the price of the product
A Glide database, called Sales Lines. The settings box for adding a new JSON Template column is open.

Make sure everything except name and unit is a numeric value.

  • If the column is a basic column (like qty or discount), you’ll see a 123 icon left of the column name.
  • If the value is a lookup from the Products database (magnifying glass icon), go to Products and check the column type there (like price).
A section of a Glide database, showing product, order and salesLine table columns.

The line_amount column is a Math column (a column designed for performing calculations), which you can spot by the math symbols icon.

A section of a Glide database, highlighting a column called line_amount and it's settings.

Your JSON should look something like the one in the screenshot below.

A section of a Glide database, highlighting a JSON Template column, called api / json.

Now that the line items JSON is set up, we need to pull it into the Orders table to structure the full payload for our integration. Since each order can have one or more line items, we’ll look up all related sales lines from the Sales Lines database and roll them up into a single field in Orders. That way, when we generate the invoice, we’re grabbing all the line items for that order in one structured JSON payload.

Preparing the JSON payload in the Orders database

In our Orders database, we have some customer info, but not everything we need for the invoice. Specifically, we’re missing the customer's name and address details. Let’s quickly add those:

  • customer / name
  • address / street
  • address / postal_code
  • address / city
  • address / country

All of these should be text fields.

A section of a Glide database showing a column for customer name and a column for address, which contains 4 sub-columns for street, postal code, city and country.

Add some sample name and address details into the orders that have at least one line item - these are the ones we’ll use for testing our integration later on.

A section of a Glide database, called Orders, showing three main columns: salesLine, customer and address.

Now it’s time to structure our payload. First, let’s bring in the Sales Lines JSON by adding a Joined List column. The Joined List computed column joins or concatenates multiple values into a single text value. This function is exactly what we need since we’re pulling all sales lines for an order and merging them into a single JSON payload.

A section of a Glide database, called Orders. The settings for adding a Joined List column, called api / line_items, are open.

For the delimiter (the Separated by field just below the Join the texts in), add a comma to make sure the structure follows proper JSON syntax. Click Save.

A section of a Glide database, called Orders. The settings for adding a Joined List column, called api / line_items, are open.

Now, we’ll structure the final payload. Add a new Template (not JSON Template) column and paste the following JSON:

{
    "data": {
        "invoiceId": "$invoiceId",
        "invoiceDate": "$invoiceDate",
        "customerName": "$customerName",
        "street": "$street",
        "postalCode": "$postalCode",
        "city": "$city",
        "country": "$country",
        "email": "$email",
        "products": [$lineItems],
        "total": $orderTotal
    },
    "convertTo": "pdf",
 
   "reportName": "$documentTitle"
}
🤔
You might be wondering why we used the Template column type instead of the JSON Template column type for this. Here’s the deal: since we’re using the Joined List column to merge multiple sales lines into a single JSON structure, the result isn’t actually a JSON object—it’s just a plain text string.
A section of a Glide database, called Orders. The right side contains a box containing JSON which is associated with a Template column, called full_payload copy.

By using the Template column, we avoid the syntax error and have full control over how we structure the final payload. The only catch is that we need to manually add quotes around strings to keep the JSON valid. Once everything is set up, you can still validate the JSON using tools like JSONLint to make sure it's formatted correctly before you send the test API call to Carbone.

Before we replace the placeholder values in our Template column, there’s one important thing we need to fix. The invoice total is calculated from sales line items, and when we roll up the sales line totals, sum them, and format them with currency, the result in the Template column includes the currency symbol. But for our JSON payload, we need it as a pure number - no symbols, just the raw numeric value.

A section of a Glide database, called Orders. On the right side is a box containing JSON which is associated with a JSON Template column, called full_payload. Within the JSON box, the line "total":$190.00 has been highlighted.

To ensure that when we send the JSON, the total is recognized as a number, not a string, we’ll create a Math column. This takes the rolled-up total and converts it into a numeric value without formatting. To do this: 

  1. Add a new Math column and name it “sales_lines / total_no_currency”.
  2. Under Configuration write “total”.
  3. Under Replacements, replace total with the total rollup from the sales lines.
A section of a Glide database, called Orders. The settings box for adding a new Math column, called salesLines / total_no_currency, is open.

Now, instead of a formatted string with currency symbols, we’ll have a clean numeric value that can be used in our JSON payload.

Then, in the Template column under Values (just below the JSON Template field), bind each placeholder to the correct field in your Orders table:

  • $invoiceId → The field that holds the increment ID
  • $invoiceDate → Use the Current time dynamic value
  • $customerName → The field that holds the customer's name
  • $street → The field that holds the street address
  • $postalCode → The field that holds the postal code
  • $city → The field that holds the city
  • $country → The field that holds the country
  • $email → The field that holds the customer's email
  • $lineItems → The Joined List column that holds the merged sales lines JSON
  • $orderTotal → The field that holds the total amount for the order without the currency
  • $documentTitle → The field that holds the name of the generated PDF

This makes sure that when we send the API request, all the placeholders get replaced with the right order and customer data, and the final JSON payload is structured correctly.

Now let’s validate our payload and test it with JSONLint. Go to the payload field of an order that has sales lines, select everything with your mouse, and copy the payload.

A section of a Glide database, called Orders. On the right side is a box containing JSON which is associated with a JSON Template column, called full_payload. The entire contents of the box have been selected.

Open JSONLint, paste the payload into the input field, and click Validate JSON. If everything is correct, you’ll see a green banner below the buttons confirming that your JSON is valid.

The JSONLint homepage. JSON code has been entered in the input field.

If something’s off, compare your JSON with the snippets we provided earlier and double-check that you’ve bound the right values in your template. Mistakes usually come from missing quotes, incorrect field names, or formatting issues, so go through it carefully!

Setting up columns for rendering the PDF report

When you generate a report with Carbone, the API response gives you a one-time URL. In other words, the moment you open it, Carbone deletes it from their servers. This feature is  great for security, since your data isn’t stored long-term, but it also means we need to set up our own system to make sure we can continue to access the report.

First, we’ll store the API response from Carbone in Glide. Add a Text column to keep the full response Carbone sends back after generating the report. Name it “api / generation_response”.

This will store the entire JSON response, which includes the renderId (the unique identifier for our generated report) we’ll use to save the PDF to Glide’s storage.

A section of a Glide database, called Orders. A Text column, entitled generation_response, has been highlighted.

This is a sample response from the API call:

{
    "success": true,
    "data": {
        "renderId": "MTAuMjAuMTEuNDIgICAgxzU0qOVbMta_TSk7UUQQeQSW52b2ljZS00.pdf"
    }
}

To extract the renderId from the JSON response:

  • Add a Query JSON column and name it “api / render_id”.
  • In the JSON field, bind it to the api / generation_response column.
  • In the field below, write: “data.renderId” (we’re using dot notation to pull the renderId value nested inside the data object).
A section of a Glide database, called Orders. The settings box for a Template column, called render_id, is shown.

Now that we have the renderId, we need to build the URL to access the report. Add a Template column and name it “api / carbone_url”. Set it up like this:

This will generate a valid Carbone report URL that we can use to download the PDF before we upload it to Glide.

A section of a Glide database, called Orders. The settings box for a Template column, called carbone_url, is shown.

Since Carbone deletes reports after they’re accessed (or downloaded) for the first time, we need to store the PDF directly in Glide. Add a URL column to store the uploaded file link in Glide.

💡
TIP: If you're not using Glide as your source app, you can upload it to an external service like Google Drive instead.
A section of a Glide database, called Orders. The settings box for a URL column, called glide_url, is shown.

Now that we’ve structured our data, let’s set up the Glide automation to generate and store these PDFs in our app.

Integrating Glide with Carbone

First, head over to Workflows in Glide (top menu, next to Layout) and create a new one.

Modal box for choosing a trigger when adding a new Workflow in Glide. The App Interaction option has been highlighted.

Set the trigger to App interaction and give it a clear name, like “Carbone / Generate Report”

💡
Using “/” in the name helps keep things organized, just like folders in the Data Editor.

Once named, link the trigger to the Orders table, since that’s where we’re storing the JSON payload.

A Glide Workflow, called Carbone / Generate Report. The trigger (App Interaction) and data source (Table called Orders) have been highlighted.

Next, add a Call API action. Click on the trigger to add a new action, then click on the new action to open its settings. In the upper right corner of the configuration, change the action to Call API.

A Glide Workflow, called Carbone / Generate Report, with the configuration settings for a Call API action open in the right sidebar.

Now, set up the API request:

  • Endpoint: https://api.carbone.io/render/:yourtemplateId
  • Method: POST
  • Headers:
    • Carbone-version: 4
    • Content_Type: application/json
    • Authorization: Bearer yourAPIKey
  • Body:  bind the full_payload column from the Orders database
  • Response Body: bind the generation_response column from the Orders database
A Glide Workflow, called Carbone / Generate Report, with the configuration settings for a Call API action open in the right sidebar.

To get your API Key, go to Carbone > My Subscription and copy the key under Test API key. Replace yourAPIKey placeholder in the Authorization header with the actual key.

The Carbone Account "My Subscription" page, showing Carbone Cloud information and API Access Keys.

To get your Template ID, go to Carbone > Template Storage, find your template, copy its ID, and replace “yourtemplateId” in the Endpoint with the real one: https://api.carbone.io/render/:yourtemplateId

The Carbone Account "Template Storage" page. The Template ID for Glide Carbone Demo has been highlighted.

Before we try to upload the PDF to Glide, we need to make sure Carbone has processed the request and the Carbone URL that we’re using as the input is ready. To do this, add a Wait for Condition step. Hover over the bottom of the Call API action, click + to add a new action, then search for wait and select Wait for Condition.

How to add an action to a Glide Workflow.
Adding a "Wait for Condition" step to a Glide Workflow.

Increase the Timeout to 10 seconds (default is 3) to give Carbone enough time to return the response and Glide to extract the render id. For the Condition, set it to api / render_id is not empty. This makes sure we’re not trying to upload the file before the render id (which enables us to form the access URL) is fully formed.

A Glide Workflow, called Carbone / Generate Report. The "Wait for condition" step is highlighted and the configuration settings for that step are open in the right sidebar.

Now we need to store the generated PDF in Glide so we can access it whenever we need. Add an Upload File action by hovering over the Wait for Condition step and clicking + to add a new action.

Set it up like this:

  • URL: bind the api / carbone_url (this is what we’re uploading)
  • File Name bind the order / order_id column
  • Glide URL: bind the api / glide_url (which is where Glide will store the uploaded file).
A Glide Workflow, called Carbone / Generate Report. The "Upload File" action is highlighted and the configuration settings for that action are open in the right sidebar.

Finally, add a Show Notification action to confirm that the file has been successfully uploaded to Glide and is accessible.

Go to your Workflow Editor and hover over the bottom border of the Upload File action. Click + to add a new action, then select Show Notification from the list.

Set the message to something clear, like “Invoice uploaded”.

A Glide Workflow, called Carbone / Generate Report. The "Show notification" action is highlighted and the configuration settings for that action are open in the right sidebar.

Now that the logic is in place, let’s make it easy for users to generate and view invoices directly from Glide.

Setting up the action buttons in the Glide UI

Back in your Order app, go to the Layouts tab, select the Table component where your orders are listed, and open the settings panel. Under Columns on the right, click + Add item, choose Button, and name it "Generate Invoice". If you want, add an icon (a document or invoice icon works well).

Now, we need to hook up the button to the workflow. Set it to trigger the Carbone / Generate Report action when clicked.

Glide's Layout editor, containing a table called Orders and the configuration settings for the table in the right sidebar. The Edit Column settings for the Generate Invoice column are shown.

To make sure users don’t generate invoices when they shouldn’t, we’ll add some conditions. Click Add Condition and set it up so the button is only clickable when all required fields used in the JSON payload are filled in. Click on the Carbone / Generate Report action and set the following conditions:

Adding conditions to a button-type column in a Glide database.

Once an invoice is generated, we need a way for users to open it directly in Glide. Under Columns, click + Add item, select Button, and name it "View Invoice". For the action, select Open Link and under URL, select the column that stores the invoice link (api / glide_url).

Adding a button-type column to a Glide database, called View Invoice. The button is set to open a link when clicked.

To keep the UI clean, we’ll only show this button when there’s actually an invoice to view. Click Add Condition and set it so the button only appears when api / glide_url is not empty.

Now, the View Invoice button will only show up when the invoice is ready.

Adding a Condition to a button-type column in Glide.

Testing the integration

Time to test our integration! Click Generate Invoice on an order that has the Generate Invoice visible (meaning all the data we need for the report is there). This should trigger the workflow, send the request to Carbone, and generate the invoice.

A Glide database called Orders. The Generate Invoice column is highlighted.

Once it’s done, you’ll receive a success message and the View Invoice button will show up next to the Generate Invoice button.

A Glide database called Orders. The View Invoice column is highlighted.

When you click the View Invoice button, the PDF should open in a new tab, where you can then download it directly to your device. Since we set up the invoice to save to Glide as soon as Glide receives the URL, this is now neatly stored inside your database!

An example invoice created using Glide and Carbone.

You did it! Here’s what you can do next…

Now that your Glide + Carbone integration is up and running, you can use what you’ve learned to automate even more of your invoicing process or extend it to other document generation workflows. 

As such, here are a few ways to build useful features onto what you've already set up:

  • Automated Customer Emails – Instead of manually sending invoices, set up an automation that emails invoices to customers the moment they’re generated.
  • Payment Reminders. If you run a service-based business, automate reminders for unpaid invoices, so customers get a reminder before the due date.
  • Automated Reporting. Schedule weekly or monthly sales reports to be sent to your team, so everyone stays informed without having to pull the data themselves (or you having to do it for them).
  • Stripe Payment Links. If you generate invoices before collecting payment, integrate Glide with Stripe to automatically generate and attach payment links.
  • Beyond Invoices... Why stop at invoicing? You can use this automation to generate contracts, proposals, inventory sheets, or any other document based on your data - eliminating manual document prep entirely.

At the end of the day, the more you automate (and the more strategic you are about it!), the more time you have to redirect away from admin tasks and towards the work that uses your expertise, that you value and that you enjoy. Because let’s be real, nobody actually likes manually creating documents.

Head to our Blueprints section for more ways to increase your business productivity!

If you want to learn more about how Glide scrubs up next to other tools, check out our Glide comparison article.



About the author
Tools Team

Tools Team

The tools.dev team comprises internal tools specialists with deep expertise in the platforms used to develop and optimize these solutions.

tools.dev

Your hub for internal tools & how to build them.

Find your tool
tools.dev

Great! You’ve successfully signed up.

Welcome back! You've successfully signed in.

You've successfully subscribed to tools.dev.

Success! Check your email for magic link to sign-in.

Success! Your billing info has been updated.

Your billing was not updated.