Skip to main content

Retool

Prerequisites

  • A PolyDoc account and API key - sign up for free, no credit card required. The free plan includes 150 PDF conversions per month.
  • A Retool account - sign up for free.

Use Case 1: Generate PDFs from Internal Tool

Add a "Generate PDF" button to a Retool app. It POSTs the selected invoice header plus its line items to PolyDoc and triggers a browser download.
Table (header) + Query (line items) → REST query (PolyDoc) → Button → Download

Add a REST API resource for PolyDoc

Use invoices (one row per invoice) and invoice_items (line rows linked by invoice_number). Examples below - two tabs in Sheets; two tables or linked records elsewhere.

invoice_numberinvoice_dateinvoice_due_dateinvoice_subtotalinvoice_tax_rateinvoice_tax_amountinvoice_totalcustomer_namecustomer_emailcustomer_streetcustomer_citycustomer_country
INV-2026-0012026-01-152026-02-1412000.11201320John Doejohn.doe@example.com456 Customer AvenueBeautiful CityUK
invoice_numberquantitynamedescriptionpriceoriginal_price
INV-2026-0011Web Design ServicesCustom website design and development for company homepage800
INV-2026-0011Logo DesignProfessional logo design with 3 revision rounds300
INV-2026-0011SEO OptimizationSearch engine optimization for 10 target keywords100150

In the Retool top nav, open Resources and click Create new → Resource. Pick REST API and configure:

  • Name: PolyDoc
  • Base URL: https://api.polydoc.tech
  • Headers (three rows):
    • Authorization = Bearer YOUR_API_KEY — grab the key from the PolyDoc Dashboard.
    • Content-Type = application/json
    • X-Sandbox = true while you build (sandbox calls don't count against your quota and return a watermarked PDF).

Click Create resource. The PolyDoc resource is now available to every Retool app in your org — Step 2 binds a query to it.

retool.com

Create a query to generate PDFs

Open your Retool app. In the bottom Query panel, click + → Resource query, pick the PolyDoc resource you just created, and name the query PolyDocQuery. Configure:

  • Action type: POST
  • URL: /pdf/convert (appended to the resource base URL)
  • Body type: Raw (so we can paste a JSON body that contains nested templateData and an items array — the key-value Body UI can't express either)

Paste this as the body. It's the canonical PolyDoc source + templateData shape — read it left-to-right with the field groupings called out below:

{
"source": "[template:YOUR_TEMPLATE_ID]",
"templateData": {
"invoice_number": "{{ invoiceHeaderTable.selectedRow.invoice_number }}",
"invoice_date": "{{ invoiceHeaderTable.selectedRow.invoice_date }}",
"invoice_due_date": "{{ invoiceHeaderTable.selectedRow.invoice_due_date }}",
"customer_name": "{{ invoiceHeaderTable.selectedRow.customer_name }}",
"customer_email": "{{ invoiceHeaderTable.selectedRow.customer_email }}",
"customer_street": "{{ invoiceHeaderTable.selectedRow.customer_street }}",
"customer_city": "{{ invoiceHeaderTable.selectedRow.customer_city }}",
"customer_country": "{{ invoiceHeaderTable.selectedRow.customer_country }}",
"invoice_subtotal": {{ Number(invoiceHeaderTable.selectedRow.invoice_subtotal) }},
"invoice_tax_rate": {{ Number(invoiceHeaderTable.selectedRow.invoice_tax_rate) }},
"invoice_tax_amount": {{ Number(invoiceHeaderTable.selectedRow.invoice_tax_amount) }},
"invoice_total": {{ Number(invoiceHeaderTable.selectedRow.invoice_total) }},
"items": {{ lineItemsQuery.data.map(r => ({
quantity: Number(r.quantity),
name: r.name,
description: r.description,
price: Number(r.price),
...(r.original_price ? { original_price: Number(r.original_price) } : {})
})) }}
}
}
  • Text fields (the eight customer/invoice strings): quoted bindings, e.g. "{{ invoiceHeaderTable.selectedRow.invoice_number }}".
  • Number fields (invoice_subtotal, invoice_tax_rate, invoice_tax_amount, invoice_total): unquoted bindings, wrapped in Number(…) because Retool surfaces table cells as strings — without the cast PolyDoc receives "1200" and renders the number as text.
  • The items array: a single inline .map() that reshapes each row of lineItemsQuery (the per-invoice line-item query you wire up in Step 3) into PolyDoc's { quantity, name, description, price, original_price? } shape. Numeric fields are cast the same way; original_price is added only when present so the strikethrough lands on the right rows.

Replace YOUR_TEMPLATE_ID with the short ID of an invoice template from the PolyDoc Dashboard — see Templates for how to pick or create one.

retool.com

Build the UI

Drag three components onto the canvas — these match the names referenced in Step 2's body:

  • Table named invoiceHeaderTable, bound to your invoice-headers data source (a SQL query, a REST query, or the Google Sheets / Airtable resource of your choice). Exactly one row should be selectable at a time — that's the selectedRow Step 2 reads.
  • A second query named lineItemsQuery that returns the line items for the currently selected invoice. The usual pattern is SELECT * FROM invoice_items WHERE invoice_number = {{ invoiceHeaderTable.selectedRow.invoice_number }} (or the REST/Sheets equivalent). Set it to run automatically when inputs change so it stays in sync with the selected header.
  • A Button labelled Generate PDF. Step 4 wires its click event to PolyDocQuery.

Optional: drop a PDF Viewer component on the canvas to preview the result inline before downloading.

retool.com

Wire the button to the query

Select the Generate PDF button. In the inspector add an Event Handler on Click:

  • Action: Trigger queryPolyDocQuery
  • On the same handler, set Only run when to {{ invoiceHeaderTable.selectedRow !== null }} so clicks with no row selected stay quiet.
  • Add a second event handler that fires On success of PolyDocQuery. Action: Run script with utils.downloadFile({ data: PolyDocQuery.data, name: `invoice-${invoiceHeaderTable.selectedRow.invoice_number}.pdf`, type: 'application/pdf' }).

Hit Preview, select a row, click the button — the sandbox PDF downloads with the selected row's data baked in.

retool.com

Use Case 2: Bulk PDF Generation

Re-use the resource from Use Case 1 to generate a PDF for every row a user ticks in a table, then trigger a download for each result.
Table (multi-select) → JavaScript query (loop) → PolyDocBulkQuery (parameterised) → Downloads

Load records into a Table component

Drag a Table onto the canvas and bind it to the same invoices data source. In the inspector:

  • Name it invoicesTable.
  • Under Interaction → Row selection, switch from Single to Multiple so users can tick the rows they want PDFs for. The selected rows surface as invoicesTable.selectedRows (an array, plural).

For the loop to work cleanly, each row should already carry its line items — either denormalised in the source data, or joined in via a sibling query you reference in Step 2. Below we assume each row has an items array attached.

retool.com

Create a JavaScript query for bulk generation

Use Case 1's PolyDocQuery is hard-bound to invoiceHeaderTable.selectedRow + lineItemsQuery, so it can't be reused for arbitrary bulk rows. Create a second resource query bound to the same PolyDoc resource and name it PolyDocBulkQuery. Same action + path as Step 2 of UC1, with this body — note every reference is to rowData, an identifier Retool only resolves when the query is triggered via additionalScope:

{
"source": "[template:YOUR_TEMPLATE_ID]",
"templateData": {
"invoice_number": "{{ rowData.invoice_number }}",
"invoice_date": "{{ rowData.invoice_date }}",
"invoice_due_date": "{{ rowData.invoice_due_date }}",
"customer_name": "{{ rowData.customer_name }}",
"customer_email": "{{ rowData.customer_email }}",
"customer_street": "{{ rowData.customer_street }}",
"customer_city": "{{ rowData.customer_city }}",
"customer_country": "{{ rowData.customer_country }}",
"invoice_subtotal": {{ Number(rowData.invoice_subtotal) }},
"invoice_tax_rate": {{ Number(rowData.invoice_tax_rate) }},
"invoice_tax_amount": {{ Number(rowData.invoice_tax_amount) }},
"invoice_total": {{ Number(rowData.invoice_total) }},
"items": {{ rowData.items.map(r => ({
quantity: Number(r.quantity),
name: r.name,
description: r.description,
price: Number(r.price),
...(r.original_price ? { original_price: Number(r.original_price) } : {})
})) }}
}
}

Now create a JavaScript query named generatePdfsForSelected that iterates the table's selected rows and triggers PolyDocBulkQuery once per row, passing the row in as rowData via additionalScope:

const rows = invoicesTable.selectedRows;
const results = [];

for (const row of rows) {
const pdf = await PolyDocBulkQuery.trigger({
additionalScope: { rowData: row },
});
results.push({ row, pdf });
}

return results;
retool.com

Collect results and download

Extend the JavaScript query from Step 2 so each loop iteration also downloads the freshly-generated PDF. Replace the results.push(…) line with:

utils.downloadFile({
data: pdf,
name: `invoice-${row.invoice_number}.pdf`,
type: "application/pdf",
});
results.push(row.invoice_number);

Each click of the bulk button now drops one PDF per selected row into the user's downloads folder. For longer batches, surface progress with a Statistic component bound to {{ generatePdfsForSelected.data.length }} over {{ invoicesTable.selectedRows.length }}.

retool.com

Add a "Generate All" button

Drop a Button labelled Generate All next to the table. Wire its Click event to Trigger query generatePdfsForSelected. Disable the button when the selection is empty by setting Disabled to {{ invoicesTable.selectedRows.length === 0 }}. Users now tick rows, click once, and receive every PDF.

retool.com