Latenode
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 Latenode account - sign up for free, no credit card required.
Use Case 1: Invoice PDFs from the Latenode Database
Generate an invoice PDF from a new row in Latenode's built-in Database, pull the matching line items, build the canonical PolyDoc body in a JavaScript node, POST to PolyDoc, then email the PDF.Database trigger (invoices) → Database query (invoice_items) → JavaScript node → HTTP Request (PolyDoc) → Email
Prepare the invoices and invoice_items tables
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 Latenode, open Data Storage → Database from the left sidebar and click Create storage. Name it invoices. Inside that storage create two collections:
invoices- one object per invoice. Each object is a single JSON document carrying the 13 canonical header fields:invoice_id,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.invoice_items- one object per line item, withinvoice_id(the foreign key back to the parent invoice),quantity,name,description,price, and an optionaloriginal_pricefor discounted rows.
Click Create object in each collection and paste a JSON value matching the fields above. Seed at least one invoice with two or three line items so the scenario has data to run against in Step 6.
Add the Database trigger
Click New Scenario, name it Invoice PDF from Database, then add a Schedule trigger and pick a polling interval (e.g. every 5 minutes). The Schedule node fires on a fixed cadence; the JavaScript node in Step 3 is what reads the newly added rows out of the Data Storage created in Step 1.
Query the matching line items
After the Schedule trigger, add a JavaScript node and paste the script below. It reads from the invoices and invoice_items collections created in Step 1 using the db object Latenode injects into every JavaScript node, then picks one invoice and the line-item rows belonging to it. The JavaScript node in Step 4 takes that pair and reshapes it into the canonical PolyDoc body.
export default async function run({ data, db }) {
// Replace STORAGE_ID with your storage's UUID - open Data Storage → click
// the invoices storage; the segment after /database/ in the URL is the UUID.
const STORAGE_ID = 'YOUR_STORAGE_ID';
const database = db.database(STORAGE_ID);
// findObjects(limit, offset, filter?) returns each stored object with its
// original fields intact - no { id, value } wrapper to unpack.
const invoices = await database.collection('invoices').findObjects(50, 0);
const inv = invoices[0]; // demo: take the first.
if (!inv) return { inv: null, rows: [] };
const rows = await database.collection('invoice_items').findObjects(50, 0, {
conditions: [{
operation: 'equal',
query: { path: 'invoice_id' },
expected: { value: inv.invoice_id },
}],
});
return { inv, rows };
}
Build the PolyDoc body in a JavaScript node
Add a second JavaScript node after the read node from Step 3. It picks up the { inv, rows } pair Step 3 returned and reshapes it into the exact source + templateData body PolyDoc expects — every numeric field wrapped in Number(...) so values that round-trip through Data Storage as strings still render as numbers in the PDF.
export default async function run({ data }) {
// {{2.inv}} + {{2.rows}} = output of the read node from Step 3
const inv = data['{{2.inv}}'];
const rows = data['{{2.rows}}'] || [];
return {
source: '[template:YOUR_TEMPLATE_ID]',
templateData: {
invoice_number: inv.invoice_number,
invoice_date: inv.invoice_date,
invoice_due_date: inv.invoice_due_date,
invoice_subtotal: Number(inv.invoice_subtotal),
invoice_tax_rate: Number(inv.invoice_tax_rate),
invoice_tax_amount: Number(inv.invoice_tax_amount),
invoice_total: Number(inv.invoice_total),
customer_name: inv.customer_name,
customer_email: inv.customer_email,
customer_street: inv.customer_street,
customer_city: inv.customer_city,
customer_country: inv.customer_country,
items: rows.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) }
: {}),
})),
},
};
}
Replace YOUR_TEMPLATE_ID with your own invoice template's short ID — find it in the Dashboard under Templates (e.g. jlE-whg). See the Templates section for how the invoice template consumes each templateData field.
Configure the HTTP Request
Add an HTTP Request node after the JavaScript node from Step 4 and configure:
- URL:
https://api.polydoc.tech/pdf/convert - Method: POST
- Headers
Authorization:Bearer YOUR_API_KEY(get your API key from the Dashboard)Content-Type:application/jsonX-Sandbox:true- Body type: raw JSON
- Body content:
{{4}}— the JavaScript node's return value from Step 4, which already matches the literal body shape below.
The body Latenode posts after substitution is exactly:
{
"source": "[template:YOUR_TEMPLATE_ID]",
"templateData": {
"invoice_number": "<invoice_number>",
"invoice_date": "<invoice_date>",
"invoice_due_date": "<invoice_due_date>",
"invoice_subtotal": <invoice_subtotal>,
"invoice_tax_rate": <invoice_tax_rate>,
"invoice_tax_amount": <invoice_tax_amount>,
"invoice_total": <invoice_total>,
"customer_name": "<customer_name>",
"customer_email": "<customer_email>",
"customer_street": "<customer_street>",
"customer_city": "<customer_city>",
"customer_country": "<customer_country>",
"items": [<items>]
}
}
Deliver the PDF and activate
Add an output node after the HTTP Request — a Gmail / SMTP / SendGrid send-email action, a cloud-storage upload, or any other downstream connector. For email, map:
- To:
{{3.inv.customer_email}} - Subject:
Invoice {{3.inv.invoice_number}} - Attachment filename:
invoice-{{3.inv.invoice_number}}.pdf - Attachment data: HTTP Request node body (the binary PDF returned by PolyDoc).
Click Run once to fire the scenario against the data you seeded in Step 1, confirm the email arrives with the PDF attached, then toggle the scenario Active. From this point the Schedule trigger drives a fresh PDF run on every interval.
Use Case 2: Webhook to PDF
Accept an external webhook (a form, a CRM, your own backend), reshape the payload with JavaScript, and generate a PDF - useful when the data source isn't in Latenode's Database but is somewhere that can POST JSON.Webhook trigger → JavaScript node → HTTP Request (PolyDoc) → Deliver
Add the Webhook trigger
Create a new scenario and add the trigger node Webhook → On webhook request. Latenode generates a unique catch URL — copy it and POST one sample request from your form or backend so Latenode learns the incoming payload shape (the Data panel on the trigger then shows every field, ready to reference from the JavaScript node in Step 2).
Reshape the payload with JavaScript
Add a JavaScript node after the webhook. It reads the webhook body (node 1) and returns the canonical source + templateData body — same shape as Use Case 1 Step 4. Adjust the field names on the right-hand side of each assignment to match what your specific webhook caller sends.
export default async function run({ data }) {
const body = data['{{1.body}}']; // raw webhook payload
const items = Array.isArray(body.items) ? body.items : [];
return {
source: '[template:YOUR_TEMPLATE_ID]',
templateData: {
invoice_number: body.invoice_number,
invoice_date: body.invoice_date,
invoice_due_date: body.invoice_due_date,
invoice_subtotal: Number(body.invoice_subtotal),
invoice_tax_rate: Number(body.invoice_tax_rate),
invoice_tax_amount: Number(body.invoice_tax_amount),
invoice_total: Number(body.invoice_total),
customer_name: body.customer_name,
customer_email: body.customer_email,
customer_street: body.customer_street,
customer_city: body.customer_city,
customer_country: body.customer_country,
items: items.map(i => ({
quantity: Number(i.quantity),
name: i.name,
description: i.description,
price: Number(i.price),
...(i.original_price
? { original_price: Number(i.original_price) }
: {}),
})),
},
};
}
Add the HTTP Request
Add an HTTP Request node configured identically to Use Case 1 Step 5 — URL, method, the three headers, raw JSON body — except the body is now bound to {{2}} (the JavaScript node from Step 2 of this use case) rather than the UC1 JavaScript node.
{
"source": "[template:YOUR_TEMPLATE_ID]",
"templateData": { /* …same 13 fields as Use Case 1 Step 5… */ }
}
Deliver the PDF and activate
Add a delivery node — email, cloud-storage upload, Slack message, whatever the caller needs — and map the PDF returned by the HTTP step onto it. Click Run once, fire a test POST against the webhook URL from Step 1, confirm the PDF, then toggle the scenario Active.
{
"invoice_number": "INV-2026-001",
"invoice_date": "2026-01-15",
"invoice_due_date": "2026-02-14",
"invoice_subtotal": 1200,
"invoice_tax_rate": 0.1,
"invoice_tax_amount": 120,
"invoice_total": 1320,
"customer_name": "John Doe",
"customer_email": "john.doe@example.com",
"customer_street": "456 Customer Avenue",
"customer_city": "Beautiful City",
"customer_country": "UK",
"items": [
{"quantity": 1, "name": "Web Design Services", "description": "Custom website design and development for company homepage", "price": 800},
{"quantity": 1, "name": "Logo Design", "description": "Professional logo design with 3 revision rounds", "price": 300},
{"quantity": 1, "name": "SEO Optimization", "description": "Search engine optimization for 10 target keywords", "price": 100, "original_price": 150}
]
}