Invoice with an attachment
Attach a PDF to a transactional email, and know where the size limit is.
Attachments go in the same POST /emails request, base64-encoded. Each one is a filename, the encoded bytes and a content type; the message is assembled as a proper MIME multipart and signed like any other.
With curl
Encode the file on a single line — the decoder rejects line breaks inside the base64, and GNU base64 wraps at 76 characters by default. Stripping the newlines works the same on Linux and macOS.
PDF=$(base64 < invoice-1042.pdf | tr -d '\n')
curl -X POST https://api.rovela.dev/emails \
-H "Authorization: Bearer re_your_sending_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: invoice-1042" \
-d @- <<EOF
{
"from": "Acme Billing <billing@mail.acme.com>",
"to": "accounts@customer.com",
"subject": "Invoice INV-1042 from Acme",
"html": "<p>Hi,</p><p>Your invoice for September is attached. Total due: <strong>\$480.00</strong> by October 15.</p>",
"text": "Hi,\n\nYour invoice for September is attached. Total due: \$480.00 by October 15.",
"attachments": [
{
"filename": "INV-1042.pdf",
"content": "$PDF",
"content_type": "application/pdf"
}
]
}
EOFWithout content_type the part is sent as application/octet-stream, which many clients show as an unknown file instead of previewing it. Set it.
From Node.js
import { readFile } from "node:fs/promises"
export async function sendInvoice(invoice, pdfPath) {
const pdf = await readFile(pdfPath)
const res = await fetch("https://api.rovela.dev/emails", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.ROVELA_API_KEY,
"Content-Type": "application/json",
// The invoice number is the natural key: a retried job never bills twice.
"Idempotency-Key": "invoice-" + invoice.number,
},
body: JSON.stringify({
from: "Acme Billing <billing@mail.acme.com>",
to: invoice.billingEmail,
reply_to: "billing@acme.com",
subject: "Invoice " + invoice.number + " from Acme",
html: "<p>Your invoice " + invoice.number + " is attached.</p>",
text: "Your invoice " + invoice.number + " is attached.",
attachments: [
{
filename: invoice.number + ".pdf",
content: pdf.toString("base64"),
content_type: "application/pdf",
},
],
}),
})
if (!res.ok) throw new Error("invoice email failed: " + res.status)
return (await res.json()).id
}reply_to sends answers to a mailbox people actually read, while the message still comes from the verified sending subdomain.
The size limit
413 before anything is sent.That covers a typical invoice or receipt comfortably. For anything bigger — reports, exports, statements with scanned pages — put the file in your own storage and send a link. That is also kinder to recipients: a large attachment is copied into every mailbox it reaches, and some corporate gateways strip or quarantine them.
Good habits
- Put the essentials in the body as well — number, amount, due date. A preview pane or a phone notification will not open the PDF.
- Use a filename without spaces or personal data; it shows up in every client and in forwarded mail.
- Send invoices one request each rather than through
/emails/batch: the batch shares the same 2 MB body, and one invalid entry rejects the whole batch. See Batch and scheduled sends.