Ruby on Rails
Complete guide to integrating Unsent with Ruby on Rails for transactional and marketing emails
This guide shows you how to integrate Unsent into your Ruby on Rails application for sending transactional emails, managing contacts, and running email campaigns.
Prerequisites
Ruby 2.7+: Rails requires Ruby 2.7 or higher
Rails 6.0+: This guide assumes Rails 6.0 or newer
Unsent API Key: Generate one in your Unsent dashboard
Verified Domain: Set up a domain in the Domains section
Installation
Add the Unsent gem to your Gemfile:
gem 'unsent'Then run:
bundle installConfiguration
Environment Variables
Add your Unsent credentials to your Rails credentials or environment variables:
UNSENT_API_KEY=un_...
UNSENT_FROM_EMAIL=noreply@yourdomain.comNever commit your .env file to version control. Add it to .gitignore and use Rails credentials or environment variables in production.
Rails Initializer
Create an initializer to set up the Unsent client as a singleton:
require 'unsent'
module UnsentClient
def self.instance
@instance ||= Unsent::Client.new(
Rails.application.credentials.dig(:unsent, :api_key) || ENV['UNSENT_API_KEY']
)
end
endNow you can access the client anywhere in your Rails app:
UnsentClient.instance.emails.send(...)Quick Start
Sending a Simple Email
Send a transactional email from a controller:
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
# Send welcome email
data, error = UnsentClient.instance.emails.send({
to: @user.email,
from: ENV['UNSENT_FROM_EMAIL'],
subject: 'Welcome to Our App!',
html: "<h1>Welcome, #{@user.name}!</h1><p>Thanks for joining us.</p>",
text: "Welcome, #{@user.name}! Thanks for joining us."
})
if error
Rails.logger.error "Failed to send welcome email: #{error}"
else
Rails.logger.info "Welcome email sent: #{data['id']}"
end
redirect_to @user, notice: 'Account created successfully!'
else
render :new
end
end
endEmail Service Object Pattern
Create a dedicated service object for better organization:
class EmailService
def self.send_welcome_email(user)
data, error = UnsentClient.instance.emails.send({
to: user.email,
from: ENV['UNSENT_FROM_EMAIL'],
subject: 'Welcome to Our App!',
html: ApplicationController.render(
template: 'user_mailer/welcome',
locals: { user: user }
)
})
if error
Rails.logger.error "Failed to send welcome email to #{user.email}: #{error}"
raise "Email sending failed: #{error['message']}"
end
data
end
def self.send_password_reset(user, token)
reset_url = Rails.application.routes.url_helpers.edit_password_reset_url(
token,
host: ENV['APP_HOST']
)
data, error = UnsentClient.instance.emails.send({
to: user.email,
from: ENV['UNSENT_FROM_EMAIL'],
subject: 'Reset Your Password',
html: ApplicationController.render(
template: 'user_mailer/password_reset',
locals: { user: user, reset_url: reset_url }
)
})
handle_email_result(data, error, user.email)
end
private
def self.handle_email_result(data, error, email)
if error
Rails.logger.error "Failed to send email to #{email}: #{error}"
raise "Email sending failed"
end
data
end
endUse in your controller:
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
EmailService.send_welcome_email(@user)
redirect_to @user, notice: 'Account created!'
else
render :new
end
end
endUsing Rails Views for Email Templates
Leverage Rails view templates for your emails:
Create Email Templates
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: #0070f3; color: white; padding: 20px; text-align: center; }
.content { padding: 20px; background-color: #f9f9f9; }
.button { display: inline-block; padding: 12px 24px; background-color: #0070f3; color: white; text-decoration: none; border-radius: 6px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Welcome to <%= ENV['APP_NAME'] %>!</h1>
</div>
<div class="content">
<p>Hi <%= user.name %>,</p>
<p>Thanks for joining us! We're excited to have you on board.</p>
<p>Here's what you can do next:</p>
<ul>
<li>Complete your profile</li>
<li>Explore our features</li>
<li>Connect with other users</li>
</ul>
<p style="text-align: center; margin-top: 30px;">
<a href="<%= root_url %>" class="button">Get Started</a>
</p>
<p>If you have any questions, just reply to this email.</p>
<p>Best regards,<br>The <%= ENV['APP_NAME'] %> Team</p>
</div>
</div>
</body>
</html>Render Templates in Service
class EmailService
def self.send_welcome_email(user)
html = ApplicationController.render(
template: 'user_mailer/welcome',
layout: false,
locals: { user: user }
)
text = "Hi #{user.name},\n\nWelcome to #{ENV['APP_NAME']}! Thanks for joining us.\n\nBest regards,\nThe Team"
data, error = UnsentClient.instance.emails.send({
to: user.email,
from: ENV['UNSENT_FROM_EMAIL'],
subject: "Welcome to #{ENV['APP_NAME']}!",
html: html,
text: text
})
raise "Email failed: #{error}" if error
data
end
endBackground Jobs
Process emails asynchronously using ActiveJob:
Create Email Job
class SendEmailJob < ApplicationJob
queue_as :default
retry_on StandardError, wait: :polynomially_longer, attempts: 3
def perform(email_type, *args)
case email_type
when 'welcome'
user = User.find(args[0])
EmailService.send_welcome_email(user)
when 'password_reset'
user = User.find(args[0])
token = args[1]
EmailService.send_password_reset(user, token)
else
raise ArgumentError, "Unknown email type: #{email_type}"
end
end
endEnqueue Jobs
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
# Send email asynchronously
SendEmailJob.perform_later('welcome', @user.id)
redirect_to @user, notice: 'Account created!'
else
render :new
end
end
endWith Sidekiq
If using Sidekiq, create a dedicated worker:
class WelcomeEmailWorker
include Sidekiq::Worker
sidekiq_options retry: 3, queue: 'mailers'
def perform(user_id)
user = User.find(user_id)
EmailService.send_welcome_email(user)
end
endEnqueue the worker:
WelcomeEmailWorker.perform_async(@user.id)Email with Attachments
Send emails with file attachments:
class InvoiceService
def self.send_invoice(user, invoice)
# Generate PDF (using Prawn, WickedPDF, etc.)
pdf_content = generate_invoice_pdf(invoice)
data, error = UnsentClient.instance.emails.send({
to: user.email,
from: ENV['UNSENT_FROM_EMAIL'],
subject: "Invoice ##{invoice.number}",
html: "<p>Please find your invoice attached.</p>",
text: "Please find your invoice attached.",
attachments: [
{
filename: "invoice_#{invoice.number}.pdf",
content: Base64.strict_encode64(pdf_content),
contentType: 'application/pdf'
}
]
})
raise "Email failed: #{error}" if error
data
end
private
def self.generate_invoice_pdf(invoice)
# Your PDF generation logic here
# Example with Prawn:
# require 'prawn'
# Prawn::Document.new do |pdf|
# pdf.text "Invoice ##{invoice.number}"
# # ... more content
# end.render
end
endScheduled Emails
Schedule emails for future delivery:
class ReminderService
def self.schedule_reminder(user, event)
# Schedule for 1 day before event
scheduled_time = event.start_time - 1.day
data, error = UnsentClient.instance.emails.send({
to: user.email,
from: ENV['UNSENT_FROM_EMAIL'],
subject: "Reminder: #{event.title} tomorrow",
html: "<p>Don't forget about #{event.title} tomorrow at #{event.start_time.strftime('%I:%M %p')}</p>",
scheduledAt: scheduled_time.iso8601
})
if error
Rails.logger.error "Failed to schedule reminder: #{error}"
return nil
end
# Save email ID to cancel later if needed
event.update(reminder_email_id: data['id'])
data
end
def self.cancel_reminder(event)
return unless event.reminder_email_id
data, error = UnsentClient.instance.emails.cancel(event.reminder_email_id)
if error
Rails.logger.error "Failed to cancel reminder: #{error}"
else
event.update(reminder_email_id: nil)
end
end
endContact Management
Sync your Rails users with Unsent contact books:
module UnsentSyncable
extend ActiveSupport::Concern
included do
after_create :sync_to_unsent
after_update :update_unsent_contact, if: :should_sync_to_unsent?
end
def sync_to_unsent
return unless email.present?
data, error = UnsentClient.instance.contacts.create(
ENV['UNSENT_CONTACT_BOOK_ID'],
{
email: email,
firstName: first_name,
lastName: last_name,
subscribed: subscribed_to_marketing?,
metadata: {
user_id: id.to_s,
plan: subscription_plan,
created_at: created_at.iso8601
}
}
)
if error
Rails.logger.error "Failed to sync user #{id} to Unsent: #{error}"
else
update_column(:unsent_contact_id, data['id'])
end
end
def update_unsent_contact
return unless unsent_contact_id && email.present?
data, error = UnsentClient.instance.contacts.update(
ENV['UNSENT_CONTACT_BOOK_ID'],
unsent_contact_id,
{
email: email,
firstName: first_name,
lastName: last_name,
subscribed: subscribed_to_marketing?,
metadata: {
plan: subscription_plan,
updated_at: updated_at.iso8601
}
}
)
Rails.logger.error "Failed to update Unsent contact: #{error}" if error
end
private
def should_sync_to_unsent?
saved_change_to_email? || saved_change_to_first_name? ||
saved_change_to_last_name? || saved_change_to_subscription_plan?
end
endInclude in your User model:
class User < ApplicationRecord
include UnsentSyncable
# ... rest of your model
endCampaigns
Send email campaigns to user segments:
class CampaignService
def self.create_newsletter(title, html_content)
data, error = UnsentClient.instance.campaigns.create({
name: title,
subject: title,
html: html_content,
from: ENV['UNSENT_FROM_EMAIL'],
contactBookId: ENV['UNSENT_CONTACT_BOOK_ID'],
replyTo: ENV['UNSENT_REPLY_TO_EMAIL']
})
raise "Failed to create campaign: #{error}" if error
data
end
def self.schedule_campaign(campaign_id, send_at)
data, error = UnsentClient.instance.campaigns.schedule(
campaign_id,
{ scheduledAt: send_at.iso8601 }
)
raise "Failed to schedule campaign: #{error}" if error
data
end
endWebhooks
Handle Unsent webhook events in your Rails app:
Create Webhook Controller
module Webhooks
class UnsentController < ApplicationController
skip_before_action :verify_authenticity_token
def create
event = params[:type]
data = params[:data]
case event
when 'email.delivered'
handle_email_delivered(data)
when 'email.bounced'
handle_email_bounced(data)
when 'email.opened'
handle_email_opened(data)
when 'email.clicked'
handle_email_clicked(data)
when 'email.complained'
handle_email_complained(data)
else
Rails.logger.info "Unhandled webhook event: #{event}"
end
head :ok
rescue => e
Rails.logger.error "Webhook error: #{e.message}"
head :unprocessable_entity
end
private
def handle_email_delivered(data)
Rails.logger.info "Email #{data['emailId']} delivered to #{data['recipient']}"
# Update your records
# EmailLog.find_by(unsent_id: data['emailId'])&.update(status: 'delivered')
end
def handle_email_bounced(data)
Rails.logger.warn "Email #{data['emailId']} bounced: #{data['bounceType']}"
# Handle bounces
user = User.find_by(email: data['recipient'])
user&.update(email_bounced: true, bounce_reason: data['bounceType'])
end
def handle_email_opened(data)
Rails.logger.info "Email #{data['emailId']} opened by #{data['recipient']}"
# Track opens
# EmailLog.find_by(unsent_id: data['emailId'])&.increment!(:open_count)
end
def handle_email_clicked(data)
Rails.logger.info "Link clicked in email #{data['emailId']}: #{data['link']}"
# Track clicks
# EmailLog.find_by(unsent_id: data['emailId'])&.increment!(:click_count)
end
def handle_email_complained(data)
Rails.logger.warn "Spam complaint for email #{data['emailId']}"
# Unsubscribe user
user = User.find_by(email: data['recipient'])
user&.update(unsubscribed: true, unsubscribed_at: Time.current)
end
end
endAdd Route
Rails.application.routes.draw do
namespace :webhooks do
post 'unsent', to: 'unsent#create'
end
endRegister Webhook
Create a Rake task to register your webhook:
namespace :unsent do
desc 'Register webhook endpoint'
task register_webhook: :environment do
webhook_url = "#{ENV['APP_HOST']}/webhooks/unsent"
data, error = UnsentClient.instance.webhooks.create({
url: webhook_url,
events: [
'email.delivered',
'email.bounced',
'email.opened',
'email.clicked',
'email.complained'
]
})
if error
puts "Failed to register webhook: #{error}"
else
puts "Webhook registered successfully: #{data['id']}"
puts "URL: #{webhook_url}"
end
end
endRun the task:
rails unsent:register_webhookTesting
RSpec Examples
require 'rails_helper'
RSpec.describe EmailService do
let(:user) { create(:user, email: 'test@example.com', name: 'Test User') }
let(:client) { instance_double(Unsent::Client) }
let(:emails) { instance_double(Unsent::Emails) }
before do
allow(UnsentClient).to receive(:instance).and_return(client)
allow(client).to receive(:emails).and_return(emails)
end
describe '.send_welcome_email' do
context 'when email sends successfully' do
before do
allow(emails).to receive(:send).and_return([{ 'id' => 'email_123' }, nil])
end
it 'sends welcome email' do
result = EmailService.send_welcome_email(user)
expect(emails).to have_received(:send).with(hash_including(
to: user.email,
subject: anything
))
expect(result['id']).to eq('email_123')
end
end
context 'when email fails' do
before do
allow(emails).to receive(:send).and_return([nil, { 'message' => 'API Error' }])
end
it 'raises an error' do
expect {
EmailService.send_welcome_email(user)
}.to raise_error(/Email sending failed/)
end
end
end
endMiniTest Examples
require 'test_helper'
class EmailServiceTest < ActiveSupport::TestCase
setup do
@user = users(:one)
@client = Minitest::Mock.new
@emails = Minitest::Mock.new
UnsentClient.stub :instance, @client do
@client.expect :emails, @emails
end
end
test 'sends welcome email successfully' do
@emails.expect :send, [{ 'id' => 'email_123' }, nil], [Hash]
UnsentClient.stub :instance, @client do
@client.stub :emails, @emails do
result = EmailService.send_welcome_email(@user)
assert_equal 'email_123', result['id']
end
end
@emails.verify
end
endError Handling
Create a comprehensive error handling wrapper:
class UnsentService
class EmailError < StandardError; end
def self.send_email(params, options = {})
data, error = UnsentClient.instance.emails.send(params, options)
if error
handle_error(error, params[:to])
raise EmailError, error['message']
end
data
rescue Unsent::HTTPError => e
Rails.logger.error "Unsent HTTP Error (#{e.status_code}): #{e.error}"
raise EmailError, "Failed to send email: #{e.error['message']}"
rescue => e
Rails.logger.error "Unexpected error sending email: #{e.message}"
raise EmailError, "Unexpected error: #{e.message}"
end
private
def self.handle_error(error, recipient)
case error['code']
when 'RATE_LIMIT_EXCEEDED'
Rails.logger.warn "Rate limit exceeded for #{recipient}"
when 'DOMAIN_NOT_VERIFIED'
Rails.logger.error "Domain not verified for #{recipient}"
when 'INVALID_EMAIL'
Rails.logger.warn "Invalid email address: #{recipient}"
else
Rails.logger.error "Email error for #{recipient}: #{error}"
end
end
endBest Practices
1. Use Environment-Specific Configuration
module UnsentClient
def self.instance
@instance ||= Unsent::Client.new(
api_key,
base_url: base_url
)
end
def self.api_key
Rails.application.credentials.dig(:unsent, :api_key) || ENV['UNSENT_API_KEY']
end
def self.base_url
# Use local API in development if available
if Rails.env.development? && ENV['UNSENT_LOCAL_API']
'http://localhost:3004'
else
nil # Use default
end
end
end2. Log Email Activity
Create an EmailLog model to track sent emails:
class EmailLog < ApplicationRecord
belongs_to :user, optional: true
validates :unsent_id, :recipient, :subject, presence: true
enum status: {
pending: 0,
sent: 1,
delivered: 2,
bounced: 3,
complained: 4
}
end3. Graceful Degradation
Don't let email failures break your app:
def create
@user = User.new(user_params)
if @user.save
begin
SendEmailJob.perform_later('welcome', @user.id)
rescue => e
Rails.logger.error "Failed to queue welcome email: #{e.message}"
# Continue anyway - user account was created successfully
end
redirect_to @user, notice: 'Account created!'
else
render :new
end
endNext Steps
- Ruby SDK: Explore all SDK features
- React Email Guide: Use React for email templates (with Rails views)
- TypeScript SDK: Reference for all API capabilities
Consider using ActionMailer with a custom delivery method that uses Unsent, giving you the best of both worlds: Rails conventions with Unsent's powerful API.