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_number | invoice_date | invoice_due_date | invoice_subtotal | invoice_tax_rate | invoice_tax_amount | invoice_total | customer_name | customer_email | customer_street | customer_city | customer_country |
|---|---|---|---|---|---|---|---|---|---|---|---|
| INV-2026-001 | 2026-01-15 | 2026-02-14 | 1200 | 0.1 | 120 | 1320 | John Doe | john.doe@example.com | 456 Customer Avenue | Beautiful City | UK |
| invoice_number | quantity | name | description | price | original_price |
|---|---|---|---|---|---|
| INV-2026-001 | 1 | Web Design Services | Custom website design and development for company homepage | 800 | |
| INV-2026-001 | 1 | Logo Design | Professional logo design with 3 revision rounds | 300 | |
| INV-2026-001 | 1 | SEO Optimization | Search engine optimization for 10 target keywords | 100 | 150 |
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/jsonX-Sandbox=truewhile 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.
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
templateDataand anitemsarray — 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 inNumber(…)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 oflineItemsQuery(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_priceis 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.
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 theselectedRowStep 2 reads. - A second query named
lineItemsQuerythat returns the line items for the currently selected invoice. The usual pattern isSELECT * 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 itsclickevent toPolyDocQuery.
Optional: drop a PDF Viewer component on the canvas to preview the result inline before downloading.
Wire the button to the query
Select the Generate PDF button. In the inspector add an Event Handler on Click:
- Action: Trigger query →
PolyDocQuery - 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 withutils.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.
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
SingletoMultipleso users can tick the rows they want PDFs for. The selected rows surface asinvoicesTable.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.
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;
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 }}.
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.