Sending Emails
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.
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
Quick Start
-
Send a simple email
Use the
sendEmail()function in a Component:import 'resources'from sendEmail {subject = 'Welcome to Hantera'body = {plainText = 'Thank you for signing up!'}dynamic = {}} -
Check delivery status
Query the sending record through the Graph API:
POST https://<hantera-hostname>/resources/graphAuthorization: Bearer <YOUR TOKEN>Content-Type: application/json[{"edge": "sendings","orderBy": "createdAt desc","limit": 1,"node": {"fields": ["sendingId", "status", "createdAt", "sentAt"]}}]Terminal window curl -i -X POST \https://<hantera-hostname>/resources/graph \-H 'Authorization: Bearer <YOUR TOKEN>' \-H 'Content-Type: application/json' \-d '[{"edge": "sendings","orderBy": "createdAt desc","limit": 1,"node": {"fields": ["sendingId", "status", "createdAt", "sentAt"]}}]'Terminal window Invoke-WebRequest `-Uri "https://<hantera-hostname>/resources/graph" `-Method POST `-Headers @{Authorization="Bearer <YOUR TOKEN>"; 'Content-Type'="application/json"} `-Body '[{"edge": "sendings","filter": "recipient == 'customer@example.com'","orderBy": "createdAt desc","limit": 1,"node": {"fields": ["sendingId", "status", "createdAt", "sentAt"]}}]' -
Handle errors
Check for bounced emails:
POST https://<hantera-hostname>/resources/graphAuthorization: Bearer <YOUR TOKEN>Content-Type: application/json[{"edge": "sendings","filter": "status == 'bounced'","node": {"fields": ["sendingId", "recipient", "errorMessage", "retryCount"]}}]Terminal window curl -i -X POST \https://<hantera-hostname>/resources/graph \-H 'Authorization: Bearer <YOUR TOKEN>' \-H 'Content-Type: application/json' \-d '[{"edge": "sendings","filter": "status == 'bounced'","node": {"fields": ["sendingId", "recipient", "errorMessage", "retryCount"]}}]'Terminal window Invoke-WebRequest `-Uri "https://<hantera-hostname>/resources/graph" `-Method POST `-Headers @{Authorization="Bearer <YOUR TOKEN>"; 'Content-Type'="application/json"} `-Body '[{"edge": "sendings","filter": "status == 'bounced'","node": {"fields": ["sendingId", "recipient", "errorMessage", "retryCount"]}}]'
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 eventfrom 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 jobfrom 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:[email protected]">Contact Billing</a> </p> </div> ' } category = 'invoice' 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 joblet 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 jobfrom sendEmail { to = customer.email 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:[email protected]">contact support</a> immediately.</strong></p> ' } category = 'account_update' 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 fielduri: /resources/registry/graph/sending/fields/campaignIdspec: value: type: text source: dynamic->'campaignId'3. Query by campaign:
POST https://<hantera-hostname>/resources/graphAuthorization: Bearer <YOUR TOKEN>Content-Type: application/json
[ { "edge": "sendings", "filter": "campaignId == '12345' and status == 'sent'", "count": true, "node": { "fields": ["sendingId", "recipient", "sentAt"] } }]curl -i -X POST \ https://<hantera-hostname>/resources/graph \ -H 'Authorization: Bearer <YOUR TOKEN>' \ -H 'Content-Type: application/json' \ -d '[ { "edge": "sendings", "filter": "campaignId == '12345' and status == 'sent'", "count": true, "node": { "fields": ["sendingId", "recipient", "sentAt"] } }]'Invoke-WebRequest `-Uri "https://<hantera-hostname>/resources/graph" `-Method POST `-Headers @{Authorization="Bearer <YOUR TOKEN>"; 'Content-Type'="application/json"} `-Body '[ { "edge": "sendings", "filter": "campaignId == '12345' and status == 'sent'", "count": true, "node": { "fields": ["sendingId", "recipient", "sentAt"] } }]'Monitoring Email Delivery
Build a dashboard to monitor email health:
POST https://<hantera-hostname>/resources/graphAuthorization: Bearer <YOUR TOKEN>Content-Type: application/json
[ { "edge": "sendings", "alias": "pending", "filter": "status == 'pending'", "count": true }, { "edge": "sendings", "alias": "sent_today", "filter": "status == 'sent' and createdAt >= '2025-10-30T00:00:00Z'", "count": true }, { "edge": "sendings", "alias": "bounced_today", "filter": "status == 'bounced' and createdAt >= '2025-10-30T00:00:00Z'", "count": true }, { "edge": "sendings", "alias": "recent_failures", "filter": "status == 'bounced' and createdAt >= '2025-10-30T00:00:00Z'", "orderBy": "createdAt desc", "limit": 10, "node": { "fields": ["sendingId", "recipient", "category", "errorMessage", "retryCount"] } }]curl -i -X POST \ https://<hantera-hostname>/resources/graph \ -H 'Authorization: Bearer <YOUR TOKEN>' \ -H 'Content-Type: application/json' \ -d '[ { "edge": "sendings", "alias": "pending", "filter": "status == 'pending'", "count": true }, { "edge": "sendings", "alias": "sent_today", "filter": "status == 'sent' and createdAt >= '2025-10-30T00:00:00Z'", "count": true }, { "edge": "sendings", "alias": "bounced_today", "filter": "status == 'bounced' and createdAt >= '2025-10-30T00:00:00Z'", "count": true }, { "edge": "sendings", "alias": "recent_failures", "filter": "status == 'bounced' and createdAt >= '2025-10-30T00:00:00Z'", "orderBy": "createdAt desc", "limit": 10, "node": { "fields": ["sendingId", "recipient", "category", "errorMessage", "retryCount"] } }]'Invoke-WebRequest `-Uri "https://<hantera-hostname>/resources/graph" `-Method POST `-Headers @{Authorization="Bearer <YOUR TOKEN>"; 'Content-Type'="application/json"} `-Body '[ { "edge": "sendings", "alias": "pending", "filter": "status == 'pending'", "count": true }, { "edge": "sendings", "alias": "sent_today", "filter": "status == 'sent' and createdAt >= '2025-10-30T00:00:00Z'", "count": true }, { "edge": "sendings", "alias": "bounced_today", "filter": "status == 'bounced' and createdAt >= '2025-10-30T00:00:00Z'", "count": true }, { "edge": "sendings", "alias": "recent_failures", "filter": "status == 'bounced' and createdAt >= '2025-10-30T00:00:00Z'", "orderBy": "createdAt desc", "limit": 10, "node": { "fields": ["sendingId", "recipient", "category", "errorMessage", "retryCount"] } }]'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-suspectedlet suspiciousOrders = query orders(orderId, orderNumber, customer, total, currencyCode, createdAt) filter 'tags anyof ["fraud-suspected"] and createdAt >= today'
// Build email body with order detailslet orderList = suspiciousOrders select o => $' - Order #{o.orderNumber}: {o.total} {o.currencyCode} ({o.customer.email})' join '\n'
// Send alert only if suspicious orders foundfrom suspiciousOrders count match when count > 0 |> sendEmail { 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' 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 archivefrom sendEmail { to = customer.email 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 categoriescategory = 'order_confirmation'category = 'password_reset'category = 'shipping_notification'
// Avoid generic categoriescategory = '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 = '[email protected]'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:
POST https://<hantera-hostname>/resources/graphAuthorization: Bearer <YOUR TOKEN>Content-Type: application/json
[ { "edge": "sendings", "filter": "createdAt >= '2025-10-01T00:00:00Z'", "count": true }, { "edge": "sendings", "alias": "bounced", "filter": "createdAt >= '2025-10-01T00:00:00Z' and status == 'bounced'", "count": true }]curl -i -X POST \ https://<hantera-hostname>/resources/graph \ -H 'Authorization: Bearer <YOUR TOKEN>' \ -H 'Content-Type: application/json' \ -d '[ { "edge": "sendings", "filter": "createdAt >= '2025-10-01T00:00:00Z'", "count": true }, { "edge": "sendings", "alias": "bounced", "filter": "createdAt >= '2025-10-01T00:00:00Z' and status == 'bounced'", "count": true }]'Invoke-WebRequest `-Uri "https://<hantera-hostname>/resources/graph" `-Method POST `-Headers @{Authorization="Bearer <YOUR TOKEN>"; 'Content-Type'="application/json"} `-Body '[ { "edge": "sendings", "filter": "createdAt >= '2025-10-01T00:00:00Z'", "count": true }, { "edge": "sendings", "alias": "bounced", "filter": "createdAt >= '2025-10-01T00:00:00Z' and status == 'bounced'", "count": true }]'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:
POST https://<hantera-hostname>/resources/graphAuthorization: Bearer <YOUR TOKEN>Content-Type: application/json
[ { "edge": "sendings", "filter": "status == 'pending' and createdAt < '2025-10-30T15:00:00Z'", "node": { "fields": ["sendingId", "recipient", "createdAt", "category"] } }]curl -i -X POST \ https://<hantera-hostname>/resources/graph \ -H 'Authorization: Bearer <YOUR TOKEN>' \ -H 'Content-Type: application/json' \ -d '[ { "edge": "sendings", "filter": "status == 'pending' and createdAt < '2025-10-30T15:00:00Z'", "node": { "fields": ["sendingId", "recipient", "createdAt", "category"] } }]'Invoke-WebRequest `-Uri "https://<hantera-hostname>/resources/graph" `-Method POST `-Headers @{Authorization="Bearer <YOUR TOKEN>"; 'Content-Type'="application/json"} `-Body '[ { "edge": "sendings", "filter": "status == 'pending' and createdAt < '2025-10-30T15:00:00Z'", "node": { "fields": ["sendingId", "recipient", "createdAt", "category"] } }]'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 emaillet sendingId = sendEmail { to = order.customer.email 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:[email protected]">Contact Support</a> </p> ' } category = 'order_confirmation' dynamic = { orderId = order.id orderNumber = order.orderNumber customerId = order.customer.id orderTotal = order.total currencyCode = order.currencyCode }}
// 2. Log the sending ID for referencelogInfo $'Order confirmation email queued: {sendingId}'