Billing Email
Send a billing receipt with an invoice PDF attached after a successful payment.
Send a billing receipt with an invoice PDF attached after a successful payment.
Prerequisites
- A published transactional template with slug
billing-receiptcontaining variablescustomerName,invoiceNumber,amountPaid,billingDate, andplanName - An API key with transactional send permissions
Send with a URL attachment
If your invoice PDF is hosted at a URL (e.g. from Stripe or your own storage), pass it via the url field:
import { Owlat } from '@owlat/sdk-js'
const owlat = new Owlat('lm_live_...')
await owlat.transactional.send({
slug: 'billing-receipt',
email: 'mira@acme.io',
dataVariables: {
customerName: 'Mira Chen',
invoiceNumber: 'INV-2026-0042',
amountPaid: '$49.00',
billingDate: 'March 19, 2026',
planName: 'Pro',
},
attachments: [
{
filename: 'invoice-INV-2026-0042.pdf',
url: 'https://files.stripe.com/invoices/INV-2026-0042.pdf',
contentType: 'application/pdf',
},
],
})
curl -X POST https://your-deployment.convex.site/api/v1/transactional \
-H "Authorization: Bearer lm_live_..." \
-H "Content-Type: application/json" \
-d '{
"slug": "billing-receipt",
"email": "mira@acme.io",
"dataVariables": {
"customerName": "Mira Chen",
"invoiceNumber": "INV-2026-0042",
"amountPaid": "$49.00",
"billingDate": "March 19, 2026",
"planName": "Pro"
},
"attachments": [
{
"filename": "invoice-INV-2026-0042.pdf",
"url": "https://files.stripe.com/invoices/INV-2026-0042.pdf",
"contentType": "application/pdf"
}
]
}'
Send with a base64 attachment
If you generate the PDF in memory (e.g. with a library like pdfkit or jspdf), encode it as base64 and use the content field:
import { Owlat } from '@owlat/sdk-js'
const owlat = new Owlat('lm_live_...')
// pdfBuffer is a Buffer from your PDF generation library
const base64Pdf = pdfBuffer.toString('base64')
await owlat.transactional.send({
slug: 'billing-receipt',
email: 'mira@acme.io',
dataVariables: {
customerName: 'Mira Chen',
invoiceNumber: 'INV-2026-0042',
amountPaid: '$49.00',
billingDate: 'March 19, 2026',
planName: 'Pro',
},
attachments: [
{
filename: 'invoice-INV-2026-0042.pdf',
content: base64Pdf,
contentType: 'application/pdf',
},
],
})
Attachment limits
You can attach up to 10 files per email with a combined maximum of 10 MB. Keep invoice PDFs lean — strip unnecessary fonts and compress images.
Complete Stripe webhook handler
A real-world example that listens for Stripe's invoice.payment_succeeded event and sends the receipt:
import { Hono } from 'hono'
import Stripe from 'stripe'
import { Owlat } from '@owlat/sdk-js'
const app = new Hono()
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const owlat = new Owlat(process.env.OWLAT_API_KEY!)
app.post('/webhooks/stripe', async (c) => {
const payload = await c.req.text()
const signature = c.req.header('stripe-signature')!
// 1. Verify the Stripe signature
const event = stripe.webhooks.constructEvent(
payload,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
)
if (event.type === 'invoice.payment_succeeded') {
const invoice = event.data.object as Stripe.Invoice
// 2. Send the billing receipt
await owlat.transactional.send({
slug: 'billing-receipt',
email: invoice.customer_email!,
dataVariables: {
customerName: invoice.customer_name ?? 'Customer',
invoiceNumber: invoice.number ?? invoice.id,
amountPaid: `$${(invoice.amount_paid / 100).toFixed(2)}`,
billingDate: new Date(invoice.created * 1000).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
}),
planName: invoice.lines.data[0]?.description ?? 'Subscription',
},
attachments: invoice.invoice_pdf
? [
{
filename: `invoice-${invoice.number}.pdf`,
url: invoice.invoice_pdf,
contentType: 'application/pdf',
},
]
: [],
})
}
return c.json({ received: true })
})
Next steps
- Transactional API reference — full attachment field documentation
- Webhook Handler — track delivery status of your billing emails
- TypeScript SDK — error handling and typed responses