Skip to main content

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_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 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, with invoice_id (the foreign key back to the parent invoice), quantity, name, description, price, and an optional original_price for 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.

latenode.com

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.

latenode.com

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/json
    • X-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).

latenode.com

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}
]
}