Skip to content

How to send an email with a PDF attachment in Node.js

The invoice email and its PDF, sent together. Two ways with working code: Nodemailer plus a PDF you render, or one API call that does both.

By Saurav N, senior software engineer · Updated

· 5 min read

Nodemailer and a PDF you render

Render the PDF first (see the Node.js HTML to PDF guide), then pass the buffer in attachments. Nodemailer talks to any SMTP server: your own, Amazon SES, SendGrid’s SMTP relay and so on.

Send from a domain you have set up with SPF and DKIM, or the mail is likely to land in spam.

send.mjs
import nodemailer from 'nodemailer';

// pdf: a Buffer you rendered earlier (Puppeteer, an API, …)
const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST,
  port: 587,
  auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
});

await transporter.sendMail({
  from: 'Kestrel Foods <billing@kestrel.in>',
  to: 'priya@example.com',
  subject: 'Invoice INV-2026-0142',
  html: '<p>Hi Priya, your invoice is attached.</p>',
  attachments: [{ filename: 'INV-2026-0142.pdf', content: pdf, contentType: 'application/pdf' }],
});

What you end up maintaining

This works, and many teams run it for years. Around it sit:

  • Two templates, one for the email and one for the PDF, that have to stay in step.
  • A render step on the send path, plus retries when either the render or the SMTP send fails.
  • A record of what was sent to whom, for the day a customer asks.

One call: render, attach and send

With Templater both templates live in one workspace. One request renders the PDF from the same data, attaches it and sends the email through your SMTP server. The Idempotency-Key stops a retry from sending twice.

send-api.mjs
const res = await fetch('https://templater.nestutils.com/v1/emails/send', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.TEMPLATER_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'invoice-INV-2026-0142-issued',
  },
  body: JSON.stringify({
    template: 'invoice-email',
    to: ['priya@example.com'],
    data: { customer: { first_name: 'Priya' }, invoice: { number: 'INV-2026-0142', total: 49402.5 } },
    attachments: [{ template: 'gst-invoice' }],
  }),
});
const { data } = await res.json(); // 202: { id, status: "queued", attachments: ["gst-invoice"] }

Questions

How big can the attachment be?

SMTP servers commonly cap messages at 10 to 25 MB, and base64 encoding adds about a third. For large files, send a link instead of attaching.

Can I attach more than one PDF?

Yes. Nodemailer takes a list, and the Templater attachments field takes several PDF templates.

How do I know the email was delivered?

SMTP only tells you the server accepted it. Templater records each send’s status and can call a webhook when it changes.

Try it on your own template

Start Free