Build a form-to-PDF web service
Convert Northwind form submissions to PDFs on the fly — POST in, PDF out.
Published Oct 27, 2025
Northwind’s booking form, intake questionnaire and event sign-up sheets all end the same way — somebody needs a tidy PDF of what was submitted. Doing that by hand means opening a template, pasting in the answers, exporting, and emailing it on. Multiply that by every submission and it is an afternoon a week gone.
This script turns a Google Doc template into a tiny PDF-generation service. It
publishes a web app endpoint that accepts a JSON POST, fills the placeholders
in the template with the posted values, renders the result as a PDF, and sends
it straight back in the response. Any form, script or workflow that can make an
HTTP request now has a PDF generator on tap.
What you’ll need
- A Google Doc to act as the template, with placeholders in the body written as
{{key}}— for example{{name}},{{date}},{{total}}. The keys must match the keys in the JSON you post. - The template’s file ID, copied from its URL, set as
TEMPLATE_IDin the config block. - A Drive folder you are happy to use as scratch space — the script creates a short-lived copy of the template per request and deletes it again.
- The script deployed as a web app (covered in Run it below).
The script
// The Google Doc used as the PDF template. Body text should contain
// {{placeholder}} tokens that match the keys you POST.
const TEMPLATE_ID = '1abcPdfTemplateId';
// Prefix for the throwaway copy made on each request. Handy when
// hunting for stragglers in Drive if a run ever fails mid-way.
const TEMP_PREFIX = 'tmp-pdf-';
/**
* Web app entry point. Accepts a JSON POST, fills the template, and
* returns the rendered PDF as base64 text.
* @param {Object} e - The event object from the web app request.
* @returns {TextOutput} Base64-encoded PDF bytes.
*/
function doPost(e) {
// 1. Guard against an empty or malformed body before doing any work.
if (!e || !e.postData || !e.postData.contents) {
return ContentService.createTextOutput('No POST body received.')
.setMimeType(ContentService.MimeType.TEXT);
}
const data = JSON.parse(e.postData.contents);
// 2. Copy the template so the original is never touched.
const copy = DriveApp.getFileById(TEMPLATE_ID)
.makeCopy(TEMP_PREFIX + Date.now());
const doc = DocumentApp.openById(copy.getId());
const body = doc.getBody();
// 3. Swap every {{key}} placeholder for its posted value.
for (const [key, value] of Object.entries(data)) {
body.replaceText('{{' + key + '}}', String(value));
}
doc.saveAndClose();
// 4. Render the filled copy as a PDF, then bin the working copy.
const pdf = copy.getAs('application/pdf').getBytes();
copy.setTrashed(true);
// 5. Return the PDF as base64 so it travels safely over HTTP.
return ContentService.createTextOutput(Utilities.base64Encode(pdf))
.setMimeType(ContentService.MimeType.TEXT);
}
How it works
doPostis the web app entry point — Apps Script calls it automatically whenever the deployed URL receives an HTTPPOST. It first checks that a request body actually arrived and bails out with a plain message if not.- It parses the body as JSON, giving an object of
key: valuepairs that map directly onto the template’s placeholders. - It makes a copy of the template with
makeCopy, so the master document is never edited. The copy is named with a timestamp and theTEMP_PREFIX. - It opens the copy, then loops over every posted pair and calls
replaceTextto swap each{{key}}token for its value.replaceTextreplaces every occurrence, so a placeholder can appear more than once. - After
saveAndClose, it callsgetAs('application/pdf')to render the doc, reads the raw bytes, and immediately trashes the working copy. - The PDF bytes are base64-encoded and returned as the response body, ready for the caller to decode and save.
Example run
Post a JSON body to the deployed web app URL:
const response = UrlFetchApp.fetch(WEB_APP_URL, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify({
name: 'Acme Co.',
date: '27 October 2025',
total: '£2,400',
}),
});
// The response body is base64 — decode it back into PDF bytes.
const pdfBlob = Utilities.newBlob(
Utilities.base64Decode(response.getContentText()),
'application/pdf',
'booking.pdf'
);
DriveApp.createFile(pdfBlob);
If the template body reads Booking for {{name}} on {{date}} — total {{total}},
the returned PDF renders as Booking for Acme Co. on 27 October 2025 — total £2,400. Any placeholder with no matching key is simply left as-is.
Run it
This automation runs as a deployed web app, not on a schedule:
- In the Apps Script editor, choose Deploy → New deployment.
- Pick Web app as the type.
- Set Execute as to yourself, and Who has access to whoever needs to
call it —
Anyonefor an open endpoint, orAnyone with Google accountfor a lightly protected one. - Click Deploy, approve the authorisation prompt, and copy the web app URL. That URL is your PDF service — point any form or script at it.
Watch out for
- The response is base64 text, not a binary file. Web apps cannot stream a true binary download, so the caller must decode the body itself. If you need a direct download link instead, write the PDF to Drive and return its URL.
- A redeploy can change the URL. Use Manage deployments → Edit to push a new version to the same deployment, otherwise existing callers break.
- Placeholder keys are literal.
{{Name}}and{{name}}are different tokens — keep the casing in the template and the JSON identical. - Each request copies and renders a whole Doc, which is not instant. Under heavy
load you will hit the daily
UrlFetchand document quotas, and concurrent requests can leave orphanedtmp-pdf-copies if a run fails — sweep the scratch folder occasionally. JSON.parsethrows on a malformed body. For a public endpoint, wrap the parse in atry/catchand return a clear error so callers know what went wrong.
Related
Build a branded approval interface
Approve Northwind requests through a custom UI — clients click, decision is logged.
Updated Nov 8, 2025
Build an interactive quiz or assessment app
Run Northwind tests with scoring and feedback — questions in a Sheet, results in another.
Updated Nov 4, 2025
Build a multi-page web app with routing
Structure a real Northwind app across views — query-param routing, shared layout.
Updated Oct 31, 2025
Build an expiring secure-download generator
Issue time-limited Northwind links via a web app — token in URL, server-side check.
Updated Oct 23, 2025
Build a guided onboarding tour for Sheets
Walk Northwind's first-time users through dialogs — each step explains one feature.
Updated Oct 19, 2025