Learn how to send emails from Hantera using the centralized Sending system. This guide covers everything from basic email sending to advanced patterns like monitoring delivery status and querying by custom data.
INFO
New to Sendings? See the Sendings resource documentation for an overview of the Sending system, lifecycle, and permissions.
What is the Sending System?
Hantera's Sending system provides a queue-based approach to email delivery with:
- Asynchronous processing: Emails are queued and sent in the background
- Individual recipient tracking: One record per recipient for granular status monitoring
- Automatic retries: Failed deliveries are retried automatically with exponential backoff
- Rate limiting: Respects configured limits to avoid overwhelming mail servers
- Status tracking: Monitor pending, sent, and bounced messages via Graph API
TIP
The Sending system is designed for transactional emails (order confirmations, password resets, notifications). For bulk marketing emails, consider using a dedicated email service provider.
Quick Start
Send a simple email
Use the
sendEmail()function in a Component:
import 'resources'
from sendEmail {
to = 'customer@example.com'
subject = 'Welcome to Hantera'
body = {
plainText = 'Thank you for signing up!'
}
dynamic = {}
}Check delivery status
Query the sending record through the Graph API:
Handle errors
Check for bounced emails:
Common Use Cases
Order Confirmation
Send an order confirmation email when an order is placed:
import 'resources'
// In a component that reacts to order.confirmed event
from sendEmail {
to = order.customer.email
subject = $'Order #{order.orderNumber} Confirmed'
body = {
plainText = $'Thank you for your order #{order.orderNumber}. Total: {order.total} {order.currencyCode}'
html = $'
<h1>Order Confirmed</h1>
<p>Thank you for your order <strong>#{order.orderNumber}</strong>.</p>
<p>Order Total: <strong>{order.total} {order.currencyCode}</strong></p>
<h2>Order Details</h2>
<p>We''ll send you updates as we process your order.</p>
'
}
category = 'order_confirmation'
dynamic = {
orderId = order.id
orderNumber = order.orderNumber
customerId = order.customer.id
}
}Why this works:
- Uses text interpolation for dynamic content
- Provides both plain text and HTML versions
- Stores order references in
dynamicfor later querying - Uses a meaningful category for filtering
Invoice/Receipt Email
Send an invoice email after payment confirmation:
import 'resources'
// In a payment.confirmed job
from sendEmail {
to = order.customer.email
subject = $'Invoice for Order #{order.orderNumber}'
body = {
plainText = $'
Invoice #{invoice.invoiceNumber}
Date: {invoice.date}
Order Number: {order.orderNumber}
Customer: {order.customer.name}
Total: {order.total} {order.currencyCode}
Payment Method: {payment.method}
Thank you for your business!
'
html = $'
<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto;">
<h1 style="color:#333;">Invoice</h1>
<p><strong>Invoice Number:</strong> {invoice.invoiceNumber}<br>
<strong>Date:</strong> {invoice.date}</p>
<table style="width:100%;border-collapse:collapse;margin:20px 0;">
<tr style="background:#f5f5f5;border-bottom:2px solid #ddd;">
<th style="padding:12px;text-align:left;">Item</th>
<th style="padding:12px;text-align:center;">Qty</th>
<th style="padding:12px;text-align:right;">Price</th>
<th style="padding:12px;text-align:right;">Total</th>
</tr>
<!-- Line items would be rendered here -->
</table>
<div style="text-align:right;margin:20px 0;">
<p style="margin:5px 0;"><strong>Subtotal:</strong> {order.subtotal} {order.currencyCode}</p>
<p style="margin:5px 0;"><strong>Tax:</strong> {order.tax} {order.currencyCode}</p>
<p style="font-size:18px;margin:10px 0;"><strong>Total:</strong> {order.total} {order.currencyCode}</p>
</div>
<p style="color:#666;font-size:12px;margin-top:30px;">
Payment Method: {payment.method}<br>
Questions about your invoice? <a href="mailto:billing@mycompany.com">Contact Billing</a>
</p>
</div>
'
}
category = 'invoice'
replyTo = 'billing@mycompany.com'
dynamic = {
orderId = order.id
invoiceNumber = invoice.invoiceNumber
customerId = order.customer.id
paymentId = payment.id
}
}Tip: Store invoice number in dynamic to easily query invoices sent to customers.
Shipping Notification
Notify customers when their order ships:
import 'resources'
// In an order.shipped job
let trackingUrl = $'https://tracking.carrier.com/{trackingNumber}'
from sendEmail {
to = order.customer.email
subject = $'Your Order #{order.orderNumber} Has Shipped!'
body = {
html = $'
<h1>Your Order Has Shipped!</h1>
<p>Order Number: <strong>{order.orderNumber}</strong></p>
<p>Tracking Number: <a href="{trackingUrl}">{trackingNumber}</a></p>
<p>Estimated Delivery: <strong>{estimatedDelivery}</strong></p>
<p>Thank you for shopping with us!</p>
'
}
category = 'order_shipped'
dynamic = {
orderId = order.id
orderNumber = order.orderNumber
trackingNumber = trackingNumber
}
}Account Notifications
Send important account updates to customers with CC to support:
import 'resources'
// In an account update job
from sendEmail {
to = customer.email
cc = ['support@mycompany.com']
subject = 'Important Account Update'
body = {
plainText = $'Your account settings have been updated. If you didn''t make these changes, please contact support immediately.'
html = $'
<h2>Account Update Notification</h2>
<p>Your account settings have been updated.</p>
<p><strong>If you didn''t make these changes, please <a href="mailto:support@mycompany.com">contact support</a> immediately.</strong></p>
'
}
category = 'account_update'
replyTo = 'support@mycompany.com'
dynamic = {
customerId = customer.id
}
}Advanced Patterns
Custom Fields for Querying
Store custom data in dynamic and create Graph fields to query it:
1. Send email with custom data:
import 'resources'
from sendEmail {
to = customer.email
subject = $'Campaign: {campaignName}'
body = {
html = '<p>Special offer just for you!</p>'
}
category = 'marketing_campaign'
dynamic = {
campaignId = campaign.id
customerId = customer.id
segmentId = segment.id
}
}2. Define custom Graph fields:
# Define campaignId field
uri: /resources/registry/graph/sending/fields/campaignId
spec:
value:
type: text
source: dynamic->'campaignId'3. Query by campaign:
Monitoring Email Delivery
Build a dashboard to monitor email health:
Alert Emails with Graph Queries
Monitor for issues and send alerts when problems are detected. This example shows a scheduled job that checks for orders with a "fraud-suspected" tag and alerts the fraud team:
import 'resources'
// Query for orders tagged with fraud-suspected
let suspiciousOrders =
query orders(orderId, orderNumber, customer, total, currencyCode, createdAt)
filter 'tags anyof ["fraud-suspected"] and createdAt >= today'
// Build email body with order details
let orderList = suspiciousOrders
select o => $' - Order #{o.orderNumber}: {o.total} {o.currencyCode} ({o.customer.email})'
join '\n'
// Send alert only if suspicious orders found
from suspiciousOrders count match
when count > 0 |> sendEmail {
to = 'fraud-team@mycompany.com'
subject = $'ALERT: {count} Suspicious Orders Detected'
body = {
plainText = $'
Fraud Alert
{count} orders flagged as suspicious in the last 24 hours:
{orderList}
Review these orders immediately in the fraud dashboard.
'
html = $'
<div style="font-family:Arial,sans-serif;max-width:600px;">
<h1 style="color:#d32f2f;">⚠️ Fraud Alert</h1>
<p><strong>{count} orders</strong> flagged as suspicious in the last 24 hours:</p>
<ul style="background:#fff3cd;padding:20px;border-left:4px solid #ffc107;">
{suspiciousOrders select o => $'<li>Order <strong>#{o.orderNumber}</strong>: {o.total} {o.currencyCode} ({o.customer.email})</li>' join ''}
</ul>
<p><a href="https://portal.mycompany.com/fraud" style="display:inline-block;padding:10px 20px;background:#d32f2f;color:white;text-decoration:none;border-radius:4px;">Review in Dashboard</a></p>
</div>
'
}
category = 'fraud_alert'
replyTo = 'alerts@mycompany.com'
dynamic = {
alertType = 'fraud_suspected'
orderCount = count
triggerDate = now
}
}
|> nothing // Do nothing if no suspicious ordersKey patterns demonstrated:
- Graph integration: Query orders with specific criteria
- Conditional sending: Only send if count > 0
- Dynamic content: Build email body from query results
- Pattern matching: Handle both alert and no-alert cases
- Alert metadata: Store alert details in
dynamicfor tracking
This pattern works for any monitoring scenario:
- Low inventory alerts (stock < threshold)
- High-value orders (total > threshold)
- Failed payment attempts (status = failed)
- Abandoned carts (lastActivity < threshold)
Multi-Recipient Emails
Send to multiple recipients with best-effort CC/BCC delivery:
import 'resources'
// Send to customer with CC to sales team and BCC to archive
from sendEmail {
to = customer.email
cc = ['sales@mycompany.com', 'account-manager@mycompany.com']
bcc = ['archive@mycompany.com']
subject = 'Account Review Summary'
body = {
html = '<p>Your account review summary...</p>'
}
category = 'account_review'
dynamic = {
customerId = customer.id
reviewId = review.id
}
}Important: Only the primary to recipient gets a Sending record with status tracking. CC and BCC recipients receive the email as best-effort delivery without individual tracking. The returned sendingId tracks only the primary recipient.
Error Handling
Handle errors gracefully in your components and jobs:
import 'resources'
from sendEmail {
to = user.email
subject = 'Test Email'
body = {
plainText = 'Test message'
}
dynamic = {}
} match
Error |> {
// Log the error
logError $'Failed to queue email: {result.error.message}'
// You might want to:
// - Trigger an alert
// - Store failure in a separate system
// - Retry with different parameters
}
uuid |> {
// Success - sendingId returned
logInfo $'Email queued successfully: {result}'
}Best Practices
1. Always Provide Plain Text
Email clients vary in HTML support. Always include a plain text version:
body = {
plainText = 'Order confirmed. Order number: 12345'
html = '<h1>Order Confirmed</h1><p>Order number: <strong>12345</strong></p>'
}2. Use Meaningful Categories
Categories make filtering and reporting easier:
// Good categories
category = 'order_confirmation'
category = 'password_reset'
category = 'shipping_notification'
// Avoid generic categories
category = 'email'
category = 'notification'Reserved Prefix: The system: prefix is reserved for internal platform sendings (password resets, email validations, etc.). Using categories like system:anything will result in an error. Use descriptive app-specific categories instead.
3. Store Queryable Data
Put anything you might query in dynamic:
dynamic = {
orderId = order.id // For finding order-related emails
customerId = customer.id // For finding customer emails
orderNumber = order.orderNumber // For search by order number
campaignId = campaign.id // For campaign analysis
}4. Set Reply-To for Support Emails
Make it easy for customers to respond:
replyTo = 'support@mycompany.com'5. Use Text Interpolation
Leverage Filtrera's text interpolation for dynamic, readable content:
subject = $'Order #{order.orderNumber} Update'
body = {
html = $'
<p>Hi {customer.firstName},</p>
<p>Your order {order.orderNumber} status: <strong>{order.status}</strong></p>
'
}6. Monitor Bounce Rates
Regularly check for bounced emails to maintain list health:
A bounce rate above 5% may indicate email list quality issues.
Troubleshooting
Emails Not Sending
Problem: Emails stuck in pending status
Solutions:
- Check queue depth - if >100, processing may be backed up
- Verify SMTP configuration in system settings
- Check for background service errors in logs
- Ensure rate limits aren't too restrictive
High Bounce Rate
Problem: Many emails bouncing
Common causes:
- Invalid email addresses
- SMTP authentication failures
- Spam filter issues
- Domain reputation problems
Solutions:
- Validate email addresses before sending
- Verify SMTP credentials
- Check sender domain reputation
- Review email content for spam triggers
Delivery Delays
Problem: Emails taking too long to send
Factors:
- Queue depth (check pending count)
- Rate limiting settings (default: 60/minute)
- Processing interval (default: 10 seconds)
- Network latency to SMTP server
Monitor with:
Performance Considerations
Rate Limiting
The system enforces rate limits to prevent overwhelming mail servers:
- Default: 60 emails/minute
- Processing interval: 10 seconds
- Configurable: Contact system administrator
Queue Management
Monitor queue depth to prevent backlogs:
// Healthy queue depth: < 50 pending
// Warning: 50-100 pending
// Critical: > 100 pendingBest Practices for High Volume
If sending many emails:
- Batch operations: Send in chunks rather than all at once
- Use categories: Group related emails for better monitoring
- Monitor bounces: Remove invalid addresses quickly
- Stagger sends: Spread large campaigns over time
Next Steps
- resources.sendEmail() Reference - Complete function documentation
- Sending Graph Node - Query email delivery status
- Custom Fields - Create queryable fields on dynamic data
- Jobs - Build event-driven email workflows
Complete Example
Here's a complete example combining everything:
import 'resources'
// Component that sends order confirmation with proper tracking
// 1. Send the email
let sendingId = sendEmail {
to = order.customer.email
cc = ['sales@mycompany.com']
subject = $'Order #{order.orderNumber} Confirmed - Thank You!'
body = {
plainText = $'
Hi {order.customer.firstName},
Thank you for your order #{order.orderNumber}!
Order Total: {order.total} {order.currencyCode}
We''ll send you updates as we process your order.
Thanks,
The Team
'
html = $'
<h1>Order Confirmed!</h1>
<p>Hi {order.customer.firstName},</p>
<p>Thank you for your order <strong>#{order.orderNumber}</strong>!</p>
<table style="width:100%;border-collapse:collapse;margin:20px 0;">
<tr style="background:#f5f5f5;">
<th style="padding:10px;text-align:left;">Item</th>
<th style="padding:10px;text-align:right;">Qty</th>
<th style="padding:10px;text-align:right;">Price</th>
</tr>
<!-- Order lines would be generated here -->
</table>
<p><strong>Total: {order.total} {order.currencyCode}</strong></p>
<p>We''ll send you updates as we process your order.</p>
<p style="color:#666;font-size:12px;">
Questions? <a href="mailto:support@mycompany.com">Contact Support</a>
</p>
'
}
category = 'order_confirmation'
replyTo = 'support@mycompany.com'
dynamic = {
orderId = order.id
orderNumber = order.orderNumber
customerId = order.customer.id
orderTotal = order.total
currencyCode = order.currencyCode
}
}
// 2. Log the sending ID for reference
logInfo $'Order confirmation email queued: {sendingId}'