Merge many PDFs from a folder into one
Combine PDFs in a Northwind folder into a single deliverable — for client handovers.
Published Sep 26, 2025
At the end of a Northwind project there is usually a handover folder full of PDFs — the brief, the contract, a few sign-off forms, the final report. The client wants one file, not eleven. Apps Script cannot stitch PDFs together on its own (Drive will not do it, and there is no built-in PDF library), so the trick is to push the bytes out to a service that does and bring back a single merged blob.
This script collects every PDF in a folder, posts them as base64 to a
merge endpoint, and writes the result back to the same folder as one
deliverable. The endpoint can be anything that takes a JSON array of files and
returns a merged PDF — a hosted service like ILovePDF, a self-hosted
pdf-lib Cloud Run, or your own Cloud Function. The Apps Script side stays the
same.
What you’ll need
- A Drive folder containing the PDFs to merge, with the script account holding edit access so it can write the merged file back.
- A PDF-merge endpoint that accepts the payload below and returns
{ "merged": "<base64>" }. The script is endpoint-agnostic — anything that speaks that shape works. - The endpoint’s API key in Script Properties (most services need one) — see Store API keys and secrets securely.
The script
// External merge endpoint. Swap for your own Cloud Function or hosted
// service — the contract is described under "Watch out for".
const MERGE_API = 'https://your-pdf-service.example/merge';
// Maximum number of source PDFs to send in a single request. Most
// services cap the payload size; chunking keeps you under the limit.
const MAX_FILES_PER_REQUEST = 25;
/**
* Collects every PDF in `folderId`, sends them to the merge service, and
* writes the merged result back into the same folder.
*
* @param {string} folderId The Drive folder that holds the source PDFs.
* @param {string} outputName Filename for the merged PDF, e.g. "Handover.pdf".
* @returns {GoogleAppsScript.Drive.File|null} The merged file, or null on no input.
*/
function mergePdfsInFolder(folderId, outputName) {
const folder = DriveApp.getFolderById(folderId);
// 1. Walk the folder and grab the bytes of every PDF.
const files = folder.getFilesByType('application/pdf');
const blobs = [];
while (files.hasNext()) blobs.push(files.next().getBlob());
if (!blobs.length) {
Logger.log('No PDFs found in folder ' + folderId + ' — nothing to merge.');
return null;
}
if (blobs.length > MAX_FILES_PER_REQUEST) {
throw new Error('Too many PDFs (' + blobs.length + ') — split the folder ' +
'or raise MAX_FILES_PER_REQUEST and check your service limits.');
}
// 2. Encode each PDF as base64 and label it. The order of `payload`
// is the order of pages in the merged result.
const payload = blobs.map((b, i) => ({
name: 'file' + i + '.pdf',
content: Utilities.base64Encode(b.getBytes()),
}));
// 3. POST to the merge endpoint. Auth header is optional — only added
// if a key is stored in Script Properties.
const key = PropertiesService.getScriptProperties().getProperty('PDF_API_KEY');
const headers = key ? { Authorization: 'Bearer ' + key } : {};
const res = UrlFetchApp.fetch(MERGE_API, {
method: 'post',
contentType: 'application/json',
headers,
payload: JSON.stringify({ files: payload }),
muteHttpExceptions: true,
});
if (res.getResponseCode() >= 300) {
throw new Error('Merge failed (' + res.getResponseCode() + '): ' + res.getContentText());
}
// 4. Decode the merged PDF and drop it back into the folder.
const merged = Utilities.newBlob(
Utilities.base64Decode(JSON.parse(res.getContentText()).merged),
'application/pdf',
outputName
);
const out = folder.createFile(merged);
Logger.log('Merged ' + blobs.length + ' PDFs into ' + out.getName());
return out;
}
How it works
mergePdfsInFolderopens the folder and iterates every file with mime typeapplication/pdf, pushing each one’s blob into an array.- If the folder is empty it logs and returns
null— no wasted API call. If there are too many files it throws, because most merge services cap the request size and a 413 is harder to debug than a clear error. - The blobs are base64-encoded and wrapped in a
{ name, content }shape. The array order is the page order in the merged PDF, so name files numerically in the folder (01-brief.pdf,02-contract.pdf) if order matters. - The script reads an optional
PDF_API_KEYfrom Script Properties and adds a bearer header only when one is present, so the same code works with both authenticated and anonymous endpoints. - It posts the payload, fails fast on a non-2xx response, and decodes the
mergedbase64 string into a fresh blob with the requested filename. - That blob is written back into the source folder via
folder.createFile(merged)and the file handle is returned for the caller to share, email, or move.
Example run
A folder for Acme handover contains:
| Filename | Size |
|---|---|
| 01-brief.pdf | 240 KB |
| 02-contract.pdf | 110 KB |
| 03-final-report.pdf | 1.8 MB |
| 04-signoff.pdf | 80 KB |
Calling mergePdfsInFolder('1abcAcmeHandoverId', 'Acme handover.pdf') posts
the four PDFs to the merge service and writes Acme handover.pdf (about
2.2 MB) back into the same folder. The log line reads
Merged 4 PDFs into Acme handover.pdf, and the client gets one file
containing pages from the brief, contract, report, and sign-off in that
order.
Run it
This is usually a one-off, called at the end of a project rather than on a schedule.
- In the Apps Script editor, open the file and edit
MERGE_APIto point at your endpoint. - From a quick wrapper or the console, call
mergePdfsInFolder(folderId, 'Handover.pdf')with the right folder ID. - Approve the Drive and external-request scopes the first time.
To trigger it from the spreadsheet that tracks projects, wrap the call in a custom menu so a project manager can click Merge handover on the row they care about.
Watch out for
- The endpoint contract is the load-bearing piece. The script assumes a JSON
request shaped
{ files: [{ name, content }] }and a JSON response shaped{ merged: "<base64>" }. If your service returns raw bytes instead, swap the decode step forres.getBlob(). - Base64 inflates payloads by about a third. A 9 MB folder of PDFs becomes a
12 MB request body, which is close to the practical
UrlFetchAppceiling. Chunk in batches and merge the chunks if you outgrow that. - File order is the array order. Drive’s
getFilesByTypeorder is not guaranteed to be alphabetical — number your source files (01-,02-) so the result is predictable. muteHttpExceptions: trueplus an explicit status check is on purpose. Most services return a JSON error body on failure, and you want that body in the log, not a generic Apps Script exception.- Treat the API key the same way you would any third-party credential. Put it in Script Properties — never inline.
Related
Build a recurring file-delivery system
Drop a fresh report file into a Northwind client folder weekly — they don't even ask.
Updated Dec 15, 2025
Build a Drive search index in Sheets
Make Northwind's file metadata searchable in a Sheet — like Spotlight for Drive.
Updated Dec 7, 2025
Build a shared-folder onboarding kit
Auto-grant new Northwind hires the folders they need on day one.
Updated Nov 29, 2025
Route saved email attachments to project folders
File Gmail attachments into the right Northwind client folder based on subject keywords.
Updated Nov 25, 2025
Bundle a folder of images into one PDF
Combine Northwind scans into a single deliverable PDF using a generation service.
Updated Nov 17, 2025