Google Apps Script can generate PDFs three ways: the built-in getAs("application/pdf") blob conversion for Docs and Sheets (zero setup, limited CSS), HtmlService templates exported to PDF (more flexible, still no real browser), or a call to a hosted rendering API with UrlFetchApp when you need exact HTML and CSS fidelity. For a quick Doc-to-PDF export, getAs wins. For invoices and reports that must match a precise design, send the HTML to PDF4.dev and read back a finished PDF.
This guide shows the built-in path first because it is free and ships with the runtime, then the hosted path for when CSS accuracy matters. Both run inside the same script, save to the same Drive folder, and attach to the same email.
Which Apps Script PDF approach should you use?
The built-in blob converter is the right default for converting existing Google Docs or Sheets. Reach for a hosted Chromium API only when the output must honor modern CSS (flexbox, grid, web fonts, exact page margins). The table below maps each approach to its CSS control, setup cost, and where the bytes end up.
| Approach | CSS control | Setup | Best for |
|---|---|---|---|
DocumentApp + getAs | None (Doc styling only) | Zero | Existing Google Docs to PDF |
SpreadsheetApp export URL | None (Sheet styling only) | Low | Sheets, tables, dashboards |
HtmlService + getAs | Basic (no modern layout) | Low | Simple HTML, label-style output |
PDF4.dev via UrlFetchApp | Full (headless Chromium) | API key | Invoices, reports, branded docs |
All four approaches output a standard Blob inside Apps Script, so saving to Drive with DriveApp.createFile() or emailing with MailApp.sendEmail() is identical regardless of how the PDF was produced.
How do you convert a Google Doc to PDF in Apps Script?
Open the document by ID, call getAs("application/pdf") on its blob, then save or send the result. This is the shortest path and needs no library. The PDF inherits the Doc's own styling, so what you see in the document is what you get in the PDF.
function docToPdfInDrive() {
const docId = "YOUR_DOC_ID";
const doc = DocumentApp.openById(docId);
// getAs returns a Blob in the requested MIME type
const pdfBlob = doc.getAs("application/pdf").setName(doc.getName() + ".pdf");
// Drop it into a specific folder
const folder = DriveApp.getFolderById("YOUR_FOLDER_ID");
const file = folder.createFile(pdfBlob);
Logger.log("Saved: " + file.getUrl());
}The first run triggers an OAuth consent screen for the Drive and Gmail scopes. After that it runs unattended. Honest caveat: you cannot change the layout here. The PDF is a faithful snapshot of the Google Doc, so any design change has to happen in the Doc itself.
How do you export a Google Sheet to PDF in Apps Script?
A Sheet has no getAs("application/pdf") on the spreadsheet object directly, so the reliable way is to hit the Sheets export endpoint with UrlFetchApp and your OAuth token. This gives you query parameters for page size, orientation, gridlines, and fit-to-width, which the plain blob path does not expose.
function sheetToPdf() {
const ssId = SpreadsheetApp.getActiveSpreadsheet().getId();
const sheetId = SpreadsheetApp.getActiveSheet().getSheetId();
const url =
"https://docs.google.com/spreadsheets/d/" + ssId + "/export?" +
"format=pdf" +
"&gid=" + sheetId +
"&size=A4" +
"&portrait=true" +
"&fitw=true" + // fit to page width
"&gridlines=false" +
"&top_margin=0.5&bottom_margin=0.5&left_margin=0.5&right_margin=0.5";
const token = ScriptApp.getOAuthToken();
const response = UrlFetchApp.fetch(url, {
headers: { Authorization: "Bearer " + token },
});
const pdfBlob = response.getBlob().setName("sheet-export.pdf");
DriveApp.createFile(pdfBlob);
}This path is good for tabular data and dashboards. The caveat is the same as Docs: styling is whatever the Sheet itself renders, plus the handful of export flags above. You cannot inject a custom header, brand colors, or a precise invoice layout this way.
Need a one-off conversion outside of Apps Script? The browser-based Html To PdfTry it free turns any HTML into a PDF with no script and no Google account, which is handy for testing a layout before you wire it into a trigger.
How do you turn HtmlService output into a PDF?
Build an HTML string (or an HtmlService template), wrap it in a blob with the text/html MIME type, then call getAs("application/pdf"). This lets you generate documents from data instead of from an existing Doc, but the converter is not a browser, so modern CSS is limited.
function htmlToPdfBuiltIn() {
const data = { customer: "Acme Corp", total: "$1,500.00", invoice: "INV-001" };
const html =
"<h1>Invoice " + data.invoice + "</h1>" +
"<p>Bill to: " + data.customer + "</p>" +
"<p style='font-size:18px;font-weight:bold'>Total: " + data.total + "</p>";
const pdfBlob = Utilities
.newBlob(html, "text/html", "invoice.html")
.getAs("application/pdf")
.setName("invoice.pdf");
DriveApp.createFile(pdfBlob);
}The honest limit: Google's internal HTML-to-PDF converter supports basic block layout, simple inline styles, and tables. It drops or approximates flexbox, CSS grid, @page rules, web fonts loaded via @font-face, and most print-specific CSS. For a plain text-and-table document it is fine. For a pixel-accurate branded invoice it will disappoint, which is the reason the next section exists.
How do you render full HTML and CSS to PDF from Apps Script?
Call a hosted Chromium renderer with UrlFetchApp.fetch(). PDF4.dev runs headless Chromium (via Playwright) on the server, so your HTML renders with the same engine as Chrome: real flexbox, grid, web fonts, and exact page sizing. You POST your HTML and data, then read the response straight into a Drive file or an email attachment.
This is the path for invoices, certificates, and reports that must match a design. Apps Script handles the trigger, the data, and the delivery, PDF4.dev handles the rendering fidelity that the built-in converter cannot reach.
function pdf4ToDrive() {
const payload = {
html: "<h1 style='font-family:Inter,sans-serif;color:#7c3aed'>Hello {{name}}</h1>",
data: { name: "Acme Corp" },
delivery: "base64",
};
const response = UrlFetchApp.fetch("https://pdf4.dev/api/v1/render", {
method: "post",
contentType: "application/json",
headers: { Authorization: "Bearer p4_live_xxx" },
payload: JSON.stringify(payload),
muteHttpExceptions: true,
});
const json = JSON.parse(response.getContentText());
const bytes = Utilities.base64Decode(json.pdf_base64);
const blob = Utilities.newBlob(bytes, "application/pdf", "render.pdf");
DriveApp.createFile(blob);
}The {{name}} and {{invoice_number}} tokens are Handlebars variables. PDF4.dev compiles them with the data object server-side, so you can keep one stored template and pass only the row data per render. With delivery: "url" the response returns a signed link instead of base64, which keeps large PDFs out of the Apps Script response buffer.
Never hard-code your p4_live_ key in a shared script. Store it in Script Properties (PropertiesService.getScriptProperties()) and read it at runtime so the key never appears in source you might share or commit.
How do you generate invoices from a Google Sheet automatically?
Combine a time-driven trigger, a Sheet read, and one render call per row. Each row holds the invoice data, the handler builds the PDF through PDF4.dev, and MailApp (or DriveApp) delivers it. This is the automation most teams actually want: a spreadsheet of orders becomes a folder of branded PDFs with no manual steps.
function generateInvoicesFromSheet() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Invoices");
const rows = sheet.getDataRange().getValues();
const header = rows.shift(); // remove header row
const key = PropertiesService.getScriptProperties().getProperty("PDF4_KEY");
const folder = DriveApp.getFolderById("YOUR_FOLDER_ID");
rows.forEach(function (row) {
const data = {
invoice_number: row[0],
customer: row[1],
total: row[2],
};
const response = UrlFetchApp.fetch("https://pdf4.dev/api/v1/render", {
method: "post",
contentType: "application/json",
headers: { Authorization: "Bearer " + key },
payload: JSON.stringify({
template_id: "invoice", // a stored template in your PDF4.dev account
data: data,
delivery: "base64",
}),
muteHttpExceptions: true,
});
const json = JSON.parse(response.getContentText());
const blob = Utilities
.newBlob(Utilities.base64Decode(json.pdf_base64), "application/pdf")
.setName(data.invoice_number + ".pdf");
folder.createFile(blob);
});
}Add the schedule in the editor under Triggers, then Add Trigger, time-driven (for example, daily at 6am). Using template_id instead of inline HTML means your design lives in the PDF4.dev dashboard and the script stays a thin data loop. To set up the schedule and the data mapping in more depth, see the companion guide on how to generate PDFs from Google Sheets.
Apps Script quotas matter for batch jobs. Consumer accounts get 100 emails and 20,000 URL fetches per day, Google Workspace accounts get 1,500 emails and 100,000 URL fetches per day. For thousands of invoices, batch the rows and let the trigger run across multiple days, or move the loop server-side.
Which option should you choose?
Pick by what you already have and how much CSS control the output needs. The built-in converters are free and instant for Google's own file types. The hosted API is the answer when a precise design has to come out the other end.
- You have a finished Google Doc. Use
DocumentApp.openById(id).getAs("application/pdf"). Nothing else is simpler, and the PDF matches the Doc exactly. - You have a Sheet or a dashboard. Use the Sheets export URL with
UrlFetchAppso you get page-size and orientation flags. Good enough for tables. - You build simple HTML from data and the design is plain. Use
Utilities.newBlob(html, "text/html").getAs("application/pdf"). Accept that modern CSS will not render. - You need invoices, certificates, or reports that must match a brand. Call PDF4.dev with
UrlFetchApp. Headless Chromium gives you real flexbox, grid, web fonts, and@pagecontrol, and the same blob slots into Drive or email like the built-in paths.
For automation that runs from a spreadsheet of orders, the pattern is always the same: a time-driven trigger reads the rows, a render call produces each PDF, and DriveApp or MailApp delivers it. The only choice is the renderer, and that choice comes down to how much your CSS matters.
If your workflow already lives outside Apps Script, the same hosted render endpoint works from Airtable automations and from Zapier or Make scenarios with the same Bearer header and JSON body, so you can move the trigger without rewriting the render logic.
Free tools mentioned:
Start generating PDFs
Build PDF templates with a visual editor. Render them via API from any language in ~300ms.



