← Back to blog

Send invoice emails with one function call

Render a PDF and email it as an attachment in a single line. No manual byte handling, no attachment wiring.

Every time I've built an invoicing feature, the code looks the same. Generate the PDF. Convert to a Buffer. Build the email. Wire up the attachment. Set the content type. Handle errors for both the render and the send separately. It's 30 lines of glue code that I've written a dozen times.

So I packaged it.

import { sendPdf } from '@formepdf/resend';

const { data, error } = await sendPdf({
  resendApiKey: process.env.RESEND_API_KEY,
  from: 'Acme Corp <billing@acme.com>',
  to: customer.email,
  subject: `Invoice #${invoice.number}`,
  template: 'invoice',
  data: {
    company: 'Acme Corp',
    customer: customer.name,
    invoiceNumber: invoice.number,
    items: invoice.lineItems,
    total: invoice.total,
  },
});

PDF rendered, email sent, invoice attached. One call. Returns Resend's { data, error } directly.

Invoice PDF rendered and emailed with Forme and Resend

What's actually happening

The package does three things in sequence:

  1. Loads the invoice template and calls it with your data to get a React element
  2. Passes that element to Forme's Rust/WASM engine, which returns PDF bytes
  3. Sends those bytes to Resend's API as a Buffer attachment

No headless browser. No temp files. No intermediate storage. The PDF exists in memory for a few milliseconds between render and send.

If you don't provide email HTML, it generates a sensible default based on the template type. An invoice gets "Hi {customer}, please find your invoice attached." A receipt gets a payment confirmation. You can override with your own html, text, or even a React Email component for the email body.

Pre-rendered bytes

Most backend code renders the PDF separately - you might store it, log it, or attach it to multiple emails. Pass the bytes directly:

import { renderDocument } from '@formepdf/core';
import { sendPdf } from '@formepdf/resend';
import { InvoiceTemplate } from './templates/invoice';

const pdfBytes = await renderDocument(InvoiceTemplate(invoiceData));

// Email it
const { data, error } = await sendPdf({
  resendApiKey: process.env.RESEND_API_KEY,
  from: 'billing@acme.com',
  to: customer.email,
  subject: `Invoice #${invoice.number}`,
  pdf: pdfBytes,
  filename: `invoice-${invoice.number}.pdf`,
});

// Same bytes go to S3, a webhook, wherever
await s3.putObject({ Body: pdfBytes, ... });

Works in plain .ts files - no JSX needed at the call site.

When you need more control

Sometimes you want to add the PDF to an existing email alongside other attachments. The lower-level renderAndAttach function handles that:

import { renderAndAttach } from '@formepdf/resend';
import { Resend } from 'resend';

const resend = new Resend(process.env.RESEND_API_KEY);
const pdf = await renderAndAttach({
  template: 'invoice',
  data: invoiceData,
  filename: `invoice-${invoice.number}.pdf`,
});

await resend.emails.send({
  from: 'billing@acme.com',
  to: customer.email,
  subject: 'Your documents',
  html: myCustomEmailHtml,
  attachments: [
    pdf,
    { filename: 'terms.pdf', path: 'https://example.com/terms.pdf' },
  ],
});

renderAndAttach returns { filename, content } which is exactly what Resend's attachment API expects. You drop it into the attachments array and you're done.

Custom templates

The package ships with 5 built-in templates: invoice, receipt, report, letter, and shipping label. But you can render any Forme template:

import { sendPdf } from '@formepdf/resend';
import { ProposalTemplate } from './templates/proposal';

const { data, error } = await sendPdf({
  resendApiKey: process.env.RESEND_API_KEY,
  from: 'sales@acme.com',
  to: prospect.email,
  subject: 'Proposal for Project Atlas',
  render: () => ProposalTemplate(proposalData),
  filename: 'proposal-atlas.pdf',
  html: '<p>Hi, please find our proposal attached. Looking forward to discussing.</p>',
});

The render function takes any Forme JSX template. If you can render it with renderDocument(), you can email it with sendPdf().

Why Resend

Resend's attachment API is clean. You pass a Buffer as content and a filename and it works. No base64 encoding, no multipart form nonsense. The developer experience matches what we're going for with Forme: things that should be simple are simple.

Also, nobody on Resend's integrations page does PDF generation. There are notification tools, CMS integrations, workflow platforms. But no PDF engine. We're filling a gap that should have been filled a long time ago.

Install

npm install @formepdf/resend resend @formepdf/react @formepdf/core

The Resend SDK is a peer dependency. You probably already have it if you're sending transactional email.

GitHub | Docs