Skip to content

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

  1. 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 = {}
    }
  2. Check delivery status

    Query the sending record through the Graph API:

    POST https://<hantera-hostname>/resources/graph
    Authorization: Bearer <YOUR TOKEN>
    Content-Type: application/json
    [
    {
    "edge": "sendings",
    "filter": "recipient == '[email protected]'",
    "orderBy": "createdAt desc",
    "limit": 1,
    "node": {
    "fields": ["sendingId", "status", "createdAt", "sentAt"]
    }
    }
    ]
  3. Handle errors

    Check for bounced emails:

    POST https://<hantera-hostname>/resources/graph
    Authorization: Bearer <YOUR TOKEN>
    Content-Type: application/json
    [
    {
    "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 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 dynamic for 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:[email protected]">Contact Billing</a>
</p>
</div>
'
}
category = 'invoice'
replyTo = '[email protected]'
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
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'
replyTo = '[email protected]'
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:

POST https://<hantera-hostname>/resources/graph
Authorization: Bearer <YOUR TOKEN>
Content-Type: application/json
[
{
"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/graph
Authorization: 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"]
}
}
]

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 {
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 = '[email protected]'
dynamic = {
alertType = 'fraud_suspected'
orderCount = count
triggerDate = now
}
}
|> nothing // Do nothing if no suspicious orders

Key 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 dynamic for 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
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 = '[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/graph
Authorization: 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
}
]

A bounce rate above 5% may indicate email list quality issues.

Troubleshooting

Emails Not Sending

Problem: Emails stuck in pending status

Solutions:

  1. Check queue depth - if >100, processing may be backed up
  2. Verify SMTP configuration in system settings
  3. Check for background service errors in logs
  4. 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:

  1. Validate email addresses before sending
  2. Verify SMTP credentials
  3. Check sender domain reputation
  4. 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/graph
Authorization: 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"]
}
}
]

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 pending

Best Practices for High Volume

If sending many emails:

  1. Batch operations: Send in chunks rather than all at once
  2. Use categories: Group related emails for better monitoring
  3. Monitor bounces: Remove invalid addresses quickly
  4. Stagger sends: Spread large campaigns over time

Next Steps

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
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'
replyTo = '[email protected]'
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}'