Replace SendGrid + DocRaptor with One API Call
The Problem
You’re currently using multiple services to send emails with PDF attachments:
- SendGrid (or Mailgun/Postmark) for sending emails
- DocRaptor (or Puppeteer/wkhtmltopdf) for PDF generation
- S3 (or filesystem) for temporary PDF storage
- Glue code to stitch it all together
That’s 3-4 services, 3-4 API keys, and 25+ lines of code for every invoice email.
The Solution
FormaMail combines email sending AND document generation into a single API call. Create email and PDF templates, then send emails with dynamically generated attachments in one request.
Before vs After
Before: SendGrid + Puppeteer + S3
// BEFORE: 25+ lines across multiple services
// 1. Generate PDF with Puppeteer (or DocRaptor)
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(renderInvoiceHTML(orderData));
const pdfBuffer = await page.pdf({ format: 'A4' });
await browser.close();
// 2. Upload to S3 (temporary storage)
const s3Key = `invoices/${orderId}.pdf`;
await s3.putObject({
Bucket: 'my-bucket',
Key: s3Key,
Body: pdfBuffer,
ContentType: 'application/pdf'
}).promise();
// 3. Send email with SendGrid
await sgMail.send({
to: customer.email,
from: 'invoices@company.com',
subject: `Invoice #${orderId}`,
html: renderEmailHTML(orderData),
attachments: [{
content: pdfBuffer.toString('base64'),
filename: `invoice-${orderId}.pdf`,
type: 'application/pdf',
}]
});
// 4. Clean up S3 (often forgotten)
await s3.deleteObject({ Bucket: 'my-bucket', Key: s3Key }).promise();Problems with this approach:
- 25+ lines of code
- 3 services to maintain (SendGrid, Puppeteer, S3)
- PDF server resources (memory, CPU, browser instances)
- S3 cleanup often forgotten (storage costs add up)
- Multiple failure points
- Rate limiting across multiple services
After: FormaMail
// AFTER: One API call
const response = await fetch('https://api.formamail.com/api/emails/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FORMAMAIL_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
templateId: "order-confirmation",
to: [{ email: customer.email, name: customer.name }],
variables: {
orderId: orderId,
customerName: customer.name,
items: orderItems,
subtotal: subtotal,
tax: tax,
total: total
},
attachments: [{
attachmentTemplateId: "invoice-pdf"
}]
})
});
const result = await response.json();
console.log('Email sent:', result.emailId);Benefits:
- ~15 lines of code (80% reduction)
- 1 API key
- No PDF server to manage
- No S3 bucket needed
- Single failure point with automatic retries
- One bill, one dashboard
Step-by-Step Migration
Create Your Email Template
- Log in to the FormaMail Dashboard
- Go to Templates → Email Templates
- Click Create Template
- Design your email using the drag-and-drop editor
- Add variables like
{{customerName}},{{orderId}},{{items}} - Save and note the template ID (or use a slug like
order-confirmation)
Create Your PDF Template
- Go to Templates → Attachment Templates
- Click Create Template
- Design your invoice PDF template with its own variables
- Define variables for your PDF like
{{orderId}},{{items}}, etc. - Save and note the template ID
Key Insight: Use the same visual designer technology to create both email and PDF templates, then send emails with attachments in a single API call.
Update Your Code
Replace your SendGrid + Puppeteer code with the FormaMail API call shown above.
Key mappings:
sgMail.send({ to: ... })→to: [{ email: ... }]renderEmailHTML(data)→templateId+variablesrenderInvoiceHTML(data) + Puppeteer→attachments[0].attachmentTemplateId
Test in API Playground
- Go to the API Playground in the docs
- Enter your API key
- Test the
/api/emails/sendendpoint with your template IDs - Verify the email and PDF look correct
Deploy to Production
# Remove old dependencies
npm uninstall @sendgrid/mail puppeteer aws-sdk
# Or for Python
pip uninstall sendgrid weasyprint boto3
# FormaMail doesn't require an SDK - just HTTP requests!Clean Up Old Services
- Cancel SendGrid subscription
- Shut down PDF generation server (if using Puppeteer)
- Delete S3 bucket or cleanup Lambda
- Remove old API keys from environment variables
Migration Checklist
- Create email template in FormaMail dashboard
- Create PDF attachment template
- Map your existing variables to FormaMail template variables
- Test with FormaMail API Playground
- Replace code in staging environment
- Verify email delivery and PDF quality
- Deploy to production
- Monitor for issues (check email logs in dashboard)
- Remove old service dependencies
- Cancel old service accounts
Cost Comparison
| Service | Monthly Cost (10K emails/PDFs) | Notes |
|---|---|---|
| SendGrid Pro | ~$20/month | Email only |
| DocRaptor | ~$30/month | 10K docs |
| S3 Storage | ~$5/month | Storage + transfer |
| Old Total | ~$55/month | + infrastructure costs |
| FormaMail | ~$40/month | All-inclusive |
FormaMail credits never expire and roll over monthly. Buy what you need, use when ready.
Common Migration Questions
What about my existing templates?
You’ll need to recreate templates in FormaMail’s designer, but this is often an improvement. The visual designer makes templates easier to maintain than code-based templates.
Do I need to change my variable structure?
FormaMail supports nested objects and arrays, just like Handlebars or EJS. Your existing data structures will likely work with minimal changes.
// Your existing data structure probably works as-is
variables: {
customer: {
name: "John Doe",
email: "john@example.com"
},
items: [
{ name: "Widget", qty: 2, price: 29.99 },
{ name: "Gadget", qty: 1, price: 49.99 }
]
}
// Use in templates as:
// {{customer.name}}
// {{#each items}}{{name}} - {{qty}} x ${{price}}{{/each}}What about email tracking?
FormaMail includes built-in tracking for:
- Email opens
- Link clicks
- Delivery status
- Bounces and complaints
No additional setup required.
Can I send without PDF attachment?
Yes! The attachments array is optional. Remove it to send email-only:
{
templateId: "welcome-email",
to: [{ email: "user@example.com" }],
variables: { name: "John" }
// No attachments = email only
}