Apps Script example · 9 min read

Email PDF Attachment: Copy-Paste Apps Script Pattern

Working email pdf attachment example in Apps Script—copy-paste code, common mistakes, and when to get it built professionally.

GmailManual run

Sending an invoice usually means exporting a PDF and then switching over to an email client to attach it manually, and emailInvoiceAsPdf collapses both of those steps into a single button-press by generating the PDF and emailing it in one script run.

The manual version of this workflow is a chain of small steps - download, rename, open a mail client, attach, address, send - each one a chance to attach the wrong file or send it to the wrong recipient.

This example reuses the same export-URL technique as a standalone PDF export, but instead of saving the result to Drive, it hands the PDF blob directly to MailApp.sendEmail as an attachment, reading the recipient and invoice number straight from cells on the Invoice sheet itself.

By the end you'll be able to fill in an Invoice tab, run one function, and have that exact invoice land as a PDF in the customer's inbox within seconds.

Need this built? Hire a Google Apps Script developer →

Sheet / project setup

CellSheetPurpose
B1InvoiceInvoice number - used in both the PDF filename and the email subject
B2InvoiceRecipient email address - where the PDF gets sent
B3InvoiceSent status - overwritten with a timestamp after a successful send

What it does

The function reads the recipient's email out of B2 and exits immediately if it's blank. It then builds a PDF export URL scoped to the Invoice tab, fetches it with an OAuth-authenticated request, names the resulting blob using the invoice number from B1, and emails it as an attachment before recording a sent timestamp back in B3.

  • Recipient email is read directly from the sheet, not hard-coded anywhere in the script
  • The PDF filename incorporates the invoice number for easy identification in an inbox
  • A sent timestamp is written back to B3 as visible proof the email actually went out

Prerequisites

Because this both exports via UrlFetchApp and sends via MailApp, the script needs authorization for both Spreadsheet export scopes and Gmail sending scopes, both of which Apps Script will prompt for on first run.

  • A sheet named 'Invoice' with recipient, invoice number, and sent-status cells laid out exactly as described
  • MailApp send quota available for the invoice volume expected
  • Correct OAuth scopes granted during the authorization flow
  • A recipient email address that's actually filled in before running the function

Walkthrough

The early return guards against sending a blank-recipient email, which MailApp would otherwise reject with a runtime error rather than silently skipping - catching it explicitly here produces a cleaner failure mode.

response.getBlob().setName(...) renames the exported PDF before it's attached, which matters because the default blob name from the export endpoint is generic and unhelpful inside an email attachment list.

Edge cases

Running this function twice in a row for the same invoice will send two separate emails, since there's no check against the B3 sent-status value before proceeding - only the timestamp gets updated after each send.

  • A blank invoice number results in a PDF and email subject reading 'Invoice ' with nothing after it, rather than throwing an error
  • Non-ASCII characters in the recipient's name or company can appear correctly in the email body but should be tested if used in the filename
  • Running against an Invoice sheet with unsaved edits still pending in the browser can export a stale version if changes haven't synced yet

Testing

Fill in a test invoice with your own email address as the recipient, run the function, and confirm the email arrives with a correctly named PDF attachment matching the invoice number.

  • Leave the recipient cell blank and confirm the function exits without attempting to send anything
  • Check the PDF attachment opens correctly and displays only the Invoice tab, not the whole spreadsheet
  • Confirm B3 updates with a readable timestamp after a successful send

Hardening

Since nothing currently prevents a duplicate send, adding a status check before generating and emailing the PDF at all would avoid both wasted MailApp quota and a confused customer receiving the same invoice twice.

  • Check whether B3 already has a value and prompt for confirmation before re-sending
  • Validate the recipient's address format before attempting to send, reusing a pattern like the one in validate-email-on-entry
  • Wrap both the export fetch and the MailApp send in try/catch blocks so a failure in one doesn't leave B3 stamped as sent when it wasn't

Variations

The same export-and-attach pattern works well for any per-row or per-tab document that needs to leave the spreadsheet as a finished PDF - receipts, statements, or signed agreements - just by changing which sheet gets exported and who it's addressed to.

  • Loop over multiple rows on a billing sheet to send several invoices in one run instead of one at a time
  • CC an internal accounts-receivable address on every invoice email for a shared paper trail
  • Attach a second PDF, like payment terms, alongside the invoice in the same email

Full code: emailInvoiceAsPdf()

The function chains together the same export technique used for standalone PDF generation with a direct MailApp send, so nothing has to touch Drive at all - the PDF only ever exists in memory as a blob.

function emailInvoiceAsPdf() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var invoiceSheet = ss.getSheetByName('Invoice');
  var recipientCell = invoiceSheet.getRange('B2');
  var recipient = recipientCell.getValue();

  if (!recipient) return;

  var url = ss.getUrl().replace(/edit$/, '');
  var exportUrl = url + 'export?format=pdf&gid=' + invoiceSheet.getSheetId() +
    '&size=A4&portrait=true&fitw=true&gridlines=false';

  var token = ScriptApp.getOAuthToken();
  var response = UrlFetchApp.fetch(exportUrl, {
    headers: { Authorization: 'Bearer ' + token }
  });

  var invoiceNumber = invoiceSheet.getRange('B1').getValue();
  var pdfBlob = response.getBlob().setName('Invoice-' + invoiceNumber + '.pdf');

  MailApp.sendEmail({
    to: recipient,
    subject: 'Invoice ' + invoiceNumber,
    body: 'Please find your invoice attached as a PDF.',
    attachments: [pdfBlob],
    name: 'Billing Department'
  });

  invoiceSheet.getRange('B3').setValue('Emailed ' + new Date().toLocaleString());
}
  1. Line 5: Reads the recipient's email address directly from the Invoice sheet.
  2. Line 7: Exits immediately if no recipient is filled in, avoiding a MailApp error.
  3. Line 10: Builds the export URL scoped to just the Invoice tab using its sheetId.
  4. Line 14: Fetches the PDF export using an OAuth-authenticated request.
  5. Line 19: Renames the exported PDF blob using the invoice number for a clear attachment filename.
  6. Line 21: Sends the email with the PDF blob attached directly, no intermediate Drive file needed.
  7. Line 29: Writes a sent timestamp back into the sheet as visible confirmation the email went out.

Deploy this example

  1. 01

    Open Apps Script

    In the bound spreadsheet: Extensions → Apps Script. For standalone projects, create one at script.google.com and link your Sheet by ID.

  2. 02

    Paste and save

    Add a .gs file, paste the code below, rename constants at the top (sheet names, column letters, API property keys), then save.

  3. 03

    Authorize once

    Run the main function from the editor. Accept OAuth scopes when prompted — triggers cannot run until authorization succeeds once.

  4. 04

    Add the trigger

    Triggers → Add trigger → choose the handler function and event (time-driven, on edit, or on form submit). Delete test triggers before production.

Before emailing invoices automatically

  • 1Recipient cell (B2) confirmed populated before running
  • 2Invoice number cell (B1) confirmed populated for a clean filename and subject
  • 3MailApp send quota checked against expected invoice volume
  • 4Duplicate-send protection considered if this could be run more than once per invoice
  • 5Email body reviewed for tone and completeness
  • 6Sent-status column (B3) confirmed not colliding with other invoice data

Frequently asked questions

Generating the blob directly from the export endpoint and attaching it in memory skips an unnecessary Drive write entirely, which is faster and avoids leaving a permanent copy of every invoice cluttering a Drive folder unless you specifically want that archive.

MailApp.sendEmail will throw an exception once the quota is exceeded, which will stop the function before it reaches the B3 timestamp write, so that invoice's sent-status will correctly remain unset, signaling it still needs to be sent.

Yes - read a second address from another cell and include it as a cc field on the MailApp.sendEmail options object, rather than adding it as a second value in the to field.

Add a temporary early return right after building the subject and body, combined with Logger.log statements, to inspect exactly what would be sent without calling MailApp.sendEmail - remove the early return once you're confident it's correct.

Yes - the export always reflects whatever is currently saved on the Invoice tab at the moment the function runs, so make sure any manual edits have finished syncing (a brief pause after typing) before running the function.

Add a 'Ready to Send' checkbox or status field to the Invoice sheet and check its value at the top of the function, returning early unless it's explicitly marked ready, similar to the checkbox guard pattern used in the row-copying example.

Related examples

Want this wired into your real workflow?

I adapt these patterns to your Sheet structure, APIs, and triggers — deployed in your Google account. Fixed-scope quotes from $500 · free 30-min consult · quote within 24 hours.