Skip to content
getnextpdf.com

Email a generated PDF as a mailer attachment

Emailing an invoice, receipt, or report is one of the most common things you do with a generated PDF. The clean way to do it is to build the document, take its raw bytes, and hand those bytes straight to your mailer’s attachment API. You do not need a temporary file on disk at any point.

This is a how-to. It assumes you already know how to send mail in your framework. The NextPDF side is a single call: Document::getPdfData() returns the raw Portable Document Format (PDF) bytes as a string. The attachment side belongs entirely to your mailer — this guide uses Laravel’s Attachment::fromData() and Symfony Mailer’s Email::attach().

NextPDF does not ship a mail helper. There is no “email this PDF” method on a document, and you should be suspicious of any example that shows one. The attachment API is always your framework’s.

This page is the outbound counterpart to embedding files inside a PDF. That guide attaches files into the PDF as embedded streams; this guide attaches the finished PDF to an email. They are different operations — do not confuse the two.

Whatever else you do, the NextPDF step is the same: produce the bytes.

<?php
declare(strict_types=1);
use NextPDF\Core\Document;
// Standalone entrypoint: the static factory wires the default dependencies.
// (The bare `new Document(...)` constructor requires injected collaborators.)
$document = Document::createStandalone();
$document->addPage();
$document->cell(0, 10, 'Invoice #1042', newLine: true);
// Raw PDF bytes, built in memory. No file is written.
$bytes = $document->getPdfData();

getPdfData() builds the document and returns its bytes as a string. It writes nothing to disk and sends no Hypertext Transfer Protocol (HTTP) headers, which is exactly what you want for an attachment.

If you hold the document only through the NextPDF\Contracts\PdfDocumentInterface type (for example, a value the framework integration handed you), use the contract-level equivalent instead:

use NextPDF\Contracts\OutputDestination;
$bytes = $document->output(dest: OutputDestination::String);

output(dest: OutputDestination::String) is declared on PdfDocumentInterface and returns the same raw bytes without emitting any headers. Use getPdfData() when you hold a concrete NextPDF\Core\Document, and the output(...) form when you only have the interface.

If you use the Laravel or Symfony integration, resolve a fresh document from the container rather than constructing one directly — see Return a generated PDF from a controller for the resolution path in each framework. Everything below works the same regardless of how you obtained the document.

Laravel: attach in-memory bytes to a Mailable

Section titled “Laravel: attach in-memory bytes to a Mailable”

Laravel’s Attachment::fromData() takes a callback that returns the raw bytes, plus a filename. There is no temporary file. Implement attachments() on your Mailable and return one Attachment.

app/Mail/InvoiceMail.php
<?php
declare(strict_types=1);
namespace App\Mail;
use App\Models\Invoice;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use NextPDF\Core\Document;
final class InvoiceMail extends Mailable
{
public function __construct(private readonly Invoice $invoice) {}
public function envelope(): Envelope
{
return new Envelope(subject: "Invoice #{$this->invoice->number}");
}
public function content(): Content
{
return new Content(markdown: 'mail.invoice');
}
/** @return array<int, Attachment> */
public function attachments(): array
{
return [
Attachment::fromData(fn (): string => $this->buildPdf(), "invoice-{$this->invoice->number}.pdf")
->withMime('application/pdf'),
];
}
private function buildPdf(): string
{
// Standalone document: the static factory wires the default
// dependencies, so this example is self-contained. If you use the
// nextpdf/laravel integration, resolve a document via its documented
// binding instead — see the integration page linked below.
$document = Document::createStandalone();
$document->addPage();
$document->cell(0, 10, "Invoice #{$this->invoice->number}", newLine: true);
return $document->getPdfData();
}
}

Send it as usual:

use App\Mail\InvoiceMail;
use Illuminate\Support\Facades\Mail;
Mail::to($invoice->customerEmail)->send(new InvoiceMail($invoice));

The fromData() callback is invoked lazily when the message is built, so the PDF is generated at send time, not at construction. Set ->withMime('application/pdf') so the recipient’s client treats the part as a PDF rather than guessing from the extension. Inside a controller you can instead call $message->attachData($bytes, $name, ['mime' => 'application/pdf']) on a raw message, but Attachment::fromData() on the Mailable is the idiomatic modern form.

Symfony Mailer’s Email::attach() accepts the body as an in-memory string, with an explicit filename and content type. Again, no temporary file.

src/Mailer/InvoiceMailer.php
<?php
declare(strict_types=1);
namespace App\Mailer;
use NextPDF\Core\DocumentFactory;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
final class InvoiceMailer
{
public function __construct(
private readonly MailerInterface $mailer,
private readonly DocumentFactory $documents,
) {}
public function sendInvoice(string $to, int $invoiceId): void
{
// Build a fresh document from the factory, which wires the default
// dependencies for you. (Use Document::createStandalone() if you do
// not have the factory injected.)
$document = $this->documents->create();
$document->addPage();
$document->cell(0, 10, "Invoice #{$invoiceId}", newLine: true);
$email = (new Email())
->from('billing@example.com')
->to($to)
->subject("Invoice #{$invoiceId}")
->text('Your invoice is attached.')
->attach(
$document->getPdfData(),
"invoice-{$invoiceId}.pdf",
'application/pdf',
);
$this->mailer->send($email);
}
}

attach(string $body, ?string $name, ?string $contentType) takes the bytes directly. Pass 'application/pdf' as the third argument so the part is typed correctly. If you prefer to attach from a stream, attachFromPath() exists, but for generated content the in-memory attach() form avoids a needless round trip to disk.

Building a multi-page PDF and sending mail are both slow enough that you should not do them on the request thread. Queue the work. The pattern is to dispatch a job (or, in Laravel, queue the Mailable itself) and build the PDF on the worker.

The simplest Laravel form: make the Mailable ShouldQueue. Because the Attachment::fromData() callback runs when the queued message is built, the PDF is generated on the worker, not at dispatch.

app/Mail/InvoiceMail.php (queued)
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
final class InvoiceMail extends Mailable implements ShouldQueue
{
// ... same envelope(), content(), attachments() as above ...
}
// Dispatched to the queue; the worker builds the PDF and sends the mail.
Mail::to($invoice->customerEmail)->queue(new InvoiceMail($invoice));

For Symfony, dispatch a Messenger message carrying the identifiers (not the bytes), and let the handler build the PDF and send the email on the worker. Pass an invoice id, look the record up in the handler, and call the InvoiceMailer shown above. Symfony’s mailer is already asynchronous when a Messenger transport is configured for SendEmailMessage, so even a synchronously-called send() can be transported to a worker.

If you generate the PDF in a dedicated generation job and then mail it, see Generate a PDF in a queued job for the integration’s GeneratePdfJob / GeneratePdfMessage surface and its worker-safety rules. A common shape is: one job generates and saves the PDF, a second job (or the success callback) reads it back and mails it. When you keep the bytes in memory across a single job, you skip the file entirely.

  • Attachment, not inline. A generated invoice or report is almost always a separate downloadable file, so attach it. Reserve inline content (Content-Disposition: inline with a cid: reference) for images you embed in the HTML body — a PDF is not body content.
  • Watch the size. Email attachments are base64-encoded in transit, which inflates the payload by roughly a third. Many receiving servers cap a message at around 10–25 MB after encoding. For a large report, attach a short notification email with a signed download link instead of the file itself, and serve the PDF over HTTP — see Return a generated PDF from a controller.
  • Build once, attach once. Capture the returned bytes once into a variable and reuse that string for the message. Do not call the final output method repeatedly for one email.
  • Memory on the worker. Holding the full PDF in memory is fine for typical invoices and receipts. For very large documents on a constrained worker, save to a temporary path with save(), attach via Laravel attach($path) / Symfony Email::attachFromPath($path), then delete the file — trading a disk round trip for a lower memory peak. (For the in-memory path used everywhere else on this page, the byte-based form is Laravel Attachment::fromData() / Symfony Email::attach($bytes, 'name.pdf', 'application/pdf').)
  • Never interpolate unvalidated user input into the attachment filename. Pass a value you control (an invoice number you generated), so the recipient’s client cannot be steered by a crafted name.
  • Send each customer only their own document. Build the PDF from the authenticated subject’s records inside the job, not from an id taken on faith from the request.
  • In a queued path, log the exception class and a correlation id on failure, never the exception message or a stack trace. Never write an empty catch block around the build-and-send.