Attached
Subject: Invoice 2026-0912 from Acme, due 26 September
Invoice 2026-0912 for NGN 412,500 is attached. It is due on 26 September 2026. Pay online, or reply to this email with any question about it.
Attachment: invoice-2026-0912.pdf
An invoice email is a template, a PDF and a due date. Your app renders the PDF; the transactional email API carries it as a base64 attachment, within limits that are stated up front, sends it now or on the issue date, and reports delivery by webhook. When the file is too big to attach, the same call sends a link instead.
Three steps and one column on your invoice table. The limits decide the second step, and the code checks them before the request leaves.
In your app, with your numbering and your accounting rules. Keep the file in your storage; you will want it for the customer portal and for reminders. Notix does not generate documents.
Under 7 MB, attach: an object with filename and content as base64 in the attachments array. Over that, leave attachments out and put a download link in the template. Ten files and 10 MB per email, once decoded.
POST /v1/emails with Idempotency-Key: invoice-{id}-send so a retry never sends twice, and scheduledAt when the invoice should go out on its issue date. Store the emailId on the invoice.
curl, or the TypeScript and Python SDKs. The SDK samples check the file size and fall back to a link, and show the optional scheduledAt.
# Your app renders the PDF. Notix carries it. The attachment is base64
# in the request body, and the idempotency key names the invoice so a
# retry cannot send it twice.
PDF_B64=$(base64 < invoice-2026-0912.pdf | tr -d '\n')
curl -X POST https://app.usenotix.dev/api/v1/emails \
-H "Authorization: Bearer $NOTIX_API_KEY" \
-H "Idempotency-Key: invoice-2026-0912-send" \
-H "Content-Type: application/json" \
-d "{
\"from\": \"Acme Billing <billing@acme.com>\",
\"to\": \"accounts@example.com\",
\"replyTo\": \"billing@acme.com\",
\"subject\": \"Invoice 2026-0912 from Acme, due 26 September\",
\"templateId\": \"tpl_invoice\",
\"variables\": {
\"invoiceNumber\": \"2026-0912\",
\"amountDue\": \"NGN 412,500\",
\"dueDate\": \"26 September 2026\",
\"payUrl\": \"https://acme.com/pay/2026-0912\"
},
\"attachments\": [
{ \"filename\": \"invoice-2026-0912.pdf\", \"content\": \"$PDF_B64\" }
]
}"
# 200 { "emailId": "eml_abc123" }
# Too big: 400 with the field and the limit, e.g.
# { "path": ["attachments", 0, "content"], "message": "Attachment content must be at most 7 MB once decoded." }
import { Notix } from "notix-js";
const notix = new Notix(process.env.NOTIX_API_KEY);
const SEVEN_MB = 7 * 1024 * 1024;
export async function sendInvoice(invoiceId: string) {
const invoice = await db.invoice.findUniqueOrThrow({ where: { id: invoiceId }, include: { customer: true } });
// Your renderer, your PDF. Notix does not generate documents.
const pdf: Buffer = await renderInvoicePdf(invoice);
// Over the per-file limit? Send a link instead of the file (see below).
const attachments =
pdf.byteLength <= SEVEN_MB
? [{ filename: `invoice-${invoice.number}.pdf`, content: pdf.toString("base64") }]
: undefined;
const { data, error } = await notix.emails.send(
{
from: "Acme Billing <billing@acme.com>",
to: invoice.customer.billingEmail,
replyTo: "billing@acme.com",
subject: `Invoice ${invoice.number} from Acme, due ${formatDate(invoice.dueDate)}`,
templateId: "tpl_invoice",
variables: {
invoiceNumber: invoice.number,
amountDue: formatMoney(invoice.totalMinor, invoice.currency),
dueDate: formatDate(invoice.dueDate),
payUrl: `https://acme.com/pay/${invoice.number}`,
// The template shows this link when the PDF was too big to attach.
downloadUrl: attachments ? "" : `https://acme.com/invoices/${invoice.number}.pdf`,
},
attachments,
// Optional: send on the invoice date rather than now. ISO 8601 with an offset.
scheduledAt: invoice.issueAt.toISOString(),
},
{ idempotencyKey: `invoice-${invoice.id}-send` },
);
if (error) throw new Error(`invoice email failed: ${error.code} ${error.message}`);
await db.invoice.update({ where: { id: invoice.id }, data: { emailId: data.emailId } });
}
import base64
import os
from notix import Notix
notix = Notix(os.environ["NOTIX_API_KEY"])
SEVEN_MB = 7 * 1024 * 1024
def send_invoice(invoice_id: str) -> None:
invoice = db.invoices.get(invoice_id, include=["customer"])
# Your renderer, your PDF. Notix does not generate documents.
pdf: bytes = render_invoice_pdf(invoice)
# Over the per-file limit? Send a link instead of the file (see below).
attachments = (
[{"filename": f"invoice-{invoice.number}.pdf", "content": base64.b64encode(pdf).decode()}]
if len(pdf) <= SEVEN_MB
else None
)
payload = {
"from": "Acme Billing <billing@acme.com>",
"to": invoice.customer.billing_email,
"replyTo": "billing@acme.com",
"subject": f"Invoice {invoice.number} from Acme, due {format_date(invoice.due_date)}",
"templateId": "tpl_invoice",
"variables": {
"invoiceNumber": invoice.number,
"amountDue": format_money(invoice.total_minor, invoice.currency),
"dueDate": format_date(invoice.due_date),
"payUrl": f"https://acme.com/pay/{invoice.number}",
# The template shows this link when the PDF was too big to attach.
"downloadUrl": "" if attachments else f"https://acme.com/invoices/{invoice.number}.pdf",
},
# Optional: send on the invoice date rather than now. ISO 8601 with an offset.
"scheduledAt": invoice.issue_at.isoformat(),
}
if attachments:
payload["attachments"] = attachments
data, error = notix.emails.send(
payload=payload,
options={"idempotency_key": f"invoice-{invoice.id}-send"},
)
if error:
raise RuntimeError(f"invoice email failed: {error['code']} {error['message']}")
db.invoices.update(invoice.id, email_id=data["emailId"])
Every figure is the API’s documented limit, measured after base64 decoding. A request over any of them is refused before anything is stored, with the field and the limit named.
| Limit | Value | Notes |
|---|---|---|
| Files per email | 10 | attachments is an array of up to ten objects. |
| Filename | 255 characters | attachments[].filename. Use the invoice number: invoice-2026-0912.pdf. |
| Size per file | 7 MB, once decoded | attachments[].content is base64; a 7 MB PDF is about 9.4 MB in the request body. |
| Attachments per email | 10 MB, once decoded | The total across all files. 10 MB of attachments encodes to about 14 MB of body. |
| Request body | 20 MB | Applied at the edge. A larger request answers 413 with no JSON body. |
| Assembled message | 40 MB | Checked again once the message is built. Over that, the email is marked FAILED with a message that says so. |
| Batch of emails | 40 MB of attachments across the batch, once decoded | Up to 100 invoices in one call to /v1/emails/batch, each with its own file. |
The full request schema, the error format and the batch endpoint are in the docs; scheduling, moving and cancelling a send are on the batch and scheduled sends page.
Number, amount and due date in the subject, one way to pay in the body, and the file attached or linked.
Subject: Invoice 2026-0912 from Acme, due 26 September
Invoice 2026-0912 for NGN 412,500 is attached. It is due on 26 September 2026. Pay online, or reply to this email with any question about it.
Attachment: invoice-2026-0912.pdf
Subject: Invoice 2026-0912 from Acme, due 26 September
Invoice 2026-0912 for NGN 412,500 is ready. Download the invoice (PDF, 11 MB). It is due on 26 September 2026. Pay online, or reply to this email with any question about it.
The same template, with downloadUrl set and no attachment.
Notix sends what you give it and does not generate documents. Use your own renderer, keep the PDF in your storage, and attach the bytes as base64. The invoice number is the filename.
A one-page invoice is well under 7 MB. A statement with scans is not. Check the size before the send: attach under the limit, otherwise send a link to the file in your storage and say so in the email.
invoice-{id}-send for the first send, invoice-{id}-reminder-1 for the first reminder. A retry of the same send returns the original id; a corrected invoice is a new send with a new key and a new number.
Pass scheduledAt in ISO 8601 with an offset and the email waits in the queue until then, with its status SCHEDULED. Update or cancel it before it goes out if the invoice changes.
Accounts teams triage by subject line. Invoice number, sender, amount or due date in the subject means the email is found in a search six months later.
Open tracking on invoices produces noise from security scanners. Listen for email.delivered and email.bounced by webhook, and treat a permanent bounce as a wrong billing address to fix on the customer record.
OTPs, receipts and alerts with idempotency keys, a deliverability pre-check and a shared suppression list.
Use casesReceipts and order confirmations from one template, with an idempotency key per order so a retry never sends twice.
LearnUp to 100 messages in one call, a send scheduled for later, and what each returns and costs.
GuidesThe PHP SDK, plain cURL, or PHPMailer pointed at the relay, with the same idempotency key on every retry.
LearnHMAC-SHA256 over the timestamp and raw body, a five-minute tolerance, replay protection, and the check in four languages.
NotixA free plan with no card, and Pro at $15 a month for 50,000 emails. Only sent volume is metered.
One call with the PDF attached, limits you can see before you build, and the free plan's 5,000 emails a month to test with. No card.