Skip to main content

Sending files with your emails

Transactional emails perform best when they are lightweight. Emails with large attachments see higher spam filtering rates, slower delivery, and more bounces from mailbox size limits. That is why Nuntly optimizes for deliverability by default: inline attachments are ideal for small files like invoices or receipts, while larger files should be shared as download links. For files larger than a few hundred KB, include a download link in your email instead of attaching the file directly. This is the approach used by most production applications for form submissions, documents, photos, and reports. Why this is the better approach:
  • Better deliverability: lightweight emails land faster and are less likely to be flagged by spam filters or bounced by receiving servers.
  • No file size constraint: your users can share files of any size since storage is on your side.
  • Access control: you decide who can access the file and for how long using expiring URLs or authentication.
  • Security: you can scan files for malware before making them available for download, protecting your recipients. With inline attachments, files land directly in the recipient’s mailbox without any intermediary check.
  • Faster delivery: large attachments add latency at every step of the delivery chain. Your API request takes longer to upload to Nuntly, processing takes longer, delivery to the recipient’s mail provider is slower, and the recipient’s mail client takes longer to download the message. For a transactional email like a password reset or an order confirmation, every second counts.
Option 1: Presigned URLs with S3 Upload the file to S3, generate a presigned download URL with an expiration time, and include the link in your email.
import { PutObjectCommand, GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Nuntly } from 'nuntly';
import { randomUUID } from 'crypto';
import { readFile } from 'fs/promises';
import path from 'path';

const s3 = new S3Client({ region: 'eu-west-1' });
const nuntly = new Nuntly({ apiKey: 'ntly_xxxx' });

const BUCKET = 'my-attachments';
const LINK_EXPIRY_SECONDS = 7 * 24 * 3600; // 7 days

async function uploadAndGetUrl(filePath: string): Promise<{ url: string; filename: string }> {
  const filename = path.basename(filePath);
  const key = `attachments/${randomUUID()}/${filename}`;
  const body = await readFile(filePath);

  // Detect content type from extension
  const contentType = getContentType(filename);

  // Upload the file to S3
  await s3.send(
    new PutObjectCommand({
      Bucket: BUCKET,
      Key: key,
      Body: body,
      ContentType: contentType,
      ContentDisposition: `attachment; filename="${filename}"`,
    }),
  );

  // Generate a presigned download URL
  const url = await getSignedUrl(s3, new GetObjectCommand({ Bucket: BUCKET, Key: key }), { expiresIn: LINK_EXPIRY_SECONDS });

  return { url, filename };
}

// Upload files and send the email with download links
const files = await Promise.all([uploadAndGetUrl('/tmp/uploads/photo.jpg'), uploadAndGetUrl('/tmp/uploads/document.pdf')]);

const fileLinks = files.map((f) => `<li><a href="${f.url}">${f.filename}</a></li>`).join('\n');

await nuntly.emails.send({
  from: 'notifications@yourapp.com',
  to: 'recipient@example.com',
  subject: 'New form submission with attachments',
  html: `<p>A new form submission has been received with the following files:</p>
         <ul>${fileLinks}</ul>
         <p>These links expire in 7 days.</p>`,
});
Option 2: Authenticated download page Link to a page in your application that verifies the user’s identity before serving the file. This gives you full control over access and audit logging.
import { Nuntly } from 'nuntly';

const nuntly = new Nuntly({ apiKey: 'ntly_xxxx' });

// After uploading files to your storage, associate them
// with a submission ID in your database
const submissionId = 'abc123';

await nuntly.emails.send({
  from: 'notifications@yourapp.com',
  to: 'recipient@example.com',
  subject: 'New form submission with attachments',
  html: `<p>A new form submission has been received.</p>
         <p><a href="https://yourapp.com/submissions/${submissionId}/files">View attached files</a></p>
         <p>You will need to sign in to download the files.</p>`,
});
This second option is more secure because the download link never expires and access is controlled by your application’s authentication layer.

Inline attachments

For lightweight transactional content like invoices, receipts, or small PDF documents, inline base64 attachments work well and require no additional infrastructure. Simply include the file content as base64 in the attachments array of your request.
Two limits apply, both measured base64-encoded: the combined size of the HTML body, text body, and all attachments must not exceed 750 KB, and the attachments together must not exceed 500 KB. For anything larger, use download links as described above.

Why not just allow large attachments?

Email providers like Gmail, Outlook, and Yahoo impose their own attachment size limits (typically 20-25 MB). Large attachments also trigger more aggressive spam filtering, increase bounce rates from recipients with full mailboxes, and are more likely to be blocked or quarantined by antivirus and security gateways. By keeping transactional emails lightweight, Nuntly ensures your messages reach the inbox quickly and reliably. This is a deliberate design choice to protect your sender reputation and deliverability.

Email size limits

The following limits apply to every email sent through the Nuntly API or SMTP relay.
ResourceLimitNotes
from300 charactersSupports RFC 5322 display name format
to recipients50Each address up to 300 characters
cc recipients50Each address up to 300 characters
bcc recipients50Each address up to 300 characters
replyTo addresses50Each address up to 300 characters
subject200 characters
Body + attachments750 KBCombined html + text + all attachment content (base64-encoded)
Attachments500 KBSum of all attachment content (base64-encoded)
Tags50
Tag name256 characters
Tag value256 characters
Custom headers50 characters per key
1024 characters per value
Total email size850 KB
Scheduled emails (sent with scheduledAt) have a stricter total size limit of 210 KB. A scheduled send carrying a large attachment will be rejected even though the same email would be accepted for immediate delivery. Send large attachments immediately, or share them as download links.

Bulk email limits

ResourceLimit
Emails per bulk request20
AttachmentsNot available in bulk requests
SchedulingNot available in bulk requests

SMTP limits

ResourceLimit
Maximum email size850 KB
Body + attachments budget750 KB
Attachments budget500 KB

Sending volume

The maximum number of emails you can send per month depends on your subscription plan. See the pricing page for details. If you need a higher volume, contact us to discuss your requirements.

Usage warnings

Nuntly warns you before you run out of sending volume, so a busy day never stops your emails without notice. When your organization reaches 80% of a sending limit, you get a heads-up; when you reach 100%, you get a “limit reached” notice and new send requests are rejected (with HTTP 402, not queued for later) until the limit resets or you upgrade. Warnings are sent on two axes:
  • Monthly: every plan has a monthly volume. You are warned as you approach and reach it.
  • Daily: the Free plan also has a daily limit. Paid plans have no daily limit, so they only receive monthly warnings.
You receive each warning two ways:
  • Email to your organization owner. Each monthly warning is sent once per billing period (no repeats). The daily warning re-arms every day, but Nuntly sends at most 5 daily warning emails per month, so a run of busy days never floods your inbox.
  • In-app banner on your dashboard, shown live whenever you are at or above 80% of a limit.
The monthly limit resets at the start of your next billing period; the daily limit resets at midnight UTC. To raise a limit, upgrade your plan from the billing settings in your dashboard.