Paulo CastellanoHow to send email with Ruby on Rails using Sendkit
Set up Sendkit in your Rails app with the Ruby SDK or as an Action Mailer transport. Covers mailers, async jobs, and error handling.

Most Rails apps outgrow letter_opener and localhost SMTP pretty fast. You need transactional email that actually lands in inboxes, tracking that tells you what happened after delivery, and an integration that doesn't fight Rails conventions. ActionMailer is great for defining mailers, but the delivery backend matters more than most teams realize.
This guide sets up Sendkit in a Rails app two ways: directly through the Ruby SDK for full control, and as an Action Mailer delivery method for drop-in compatibility with your existing mailers.

Install the gem
Add Sendkit to your Gemfile:
gem "sendkit"Run bundle install. Grab an API key from your Sendkit dashboard. Keys prefixed with sk_live_ send real email. Keys prefixed with sk_test_ validate requests without delivering, which is what you want in your test suite.
Store the key in your credentials or environment:
# .env (if using dotenv)
SENDKIT_API_KEY=sk_live_your_api_keyConfigure with an initializer
Create config/initializers/sendkit.rb:
require "sendkit"
SENDKIT_CLIENT = Sendkit::Client.new(ENV.fetch("SENDKIT_API_KEY"))One client instance, initialized once at boot, reused everywhere. The client is thread-safe, so this works fine with Puma's threaded workers.
Send a basic email from a controller
The simplest path. Skip ActionMailer entirely and call the SDK directly:
class OrdersController < ApplicationController
def create
@order = Order.create!(order_params)
SENDKIT_CLIENT.emails.send(
from: "[email protected]",
to: @order.user.email,
subject: "Order ##{@order.id} confirmed",
html: render_to_string(
partial: "orders/confirmation_email",
locals: { order: @order }
)
)
redirect_to @order, notice: "Order placed."
end
endrender_to_string lets you use your existing Rails partials as email templates. No separate mailer class needed. This approach works when you have a handful of emails and don't need the full ActionMailer ceremony.
The response hash includes an id field. Store it if you want to correlate with webhook events later.

Use Action Mailer with Sendkit as delivery method
For most Rails apps, Action Mailer is the right abstraction. You get view templates, layouts, previews, and interceptors. The missing piece is a delivery method that sends through Sendkit's API instead of SMTP.
Create a custom delivery method:
# lib/sendkit_delivery_method.rb
class SendkitDeliveryMethod
attr_reader :settings
def initialize(settings)
@settings = settings
@client = Sendkit::Client.new(settings[:api_key])
end
def deliver!(mail)
params = {
from: mail.from&.first,
to: Array(mail.to),
subject: mail.subject
}
params[:html] = mail.html_part&.body&.to_s if mail.html_part
params[:text] = mail.text_part&.body&.to_s if mail.text_part
params[:html] ||= mail.body.to_s if mail.content_type&.include?("text/html")
params[:text] ||= mail.body.to_s unless params[:html]
params[:reply_to] = mail.reply_to.first if mail.reply_to&.any?
@client.emails.send(**params)
end
endRegister it in your environment config:
# config/environments/production.rb
require_relative "../../lib/sendkit_delivery_method"
ActionMailer::Base.add_delivery_method :sendkit, SendkitDeliveryMethod,
api_key: ENV.fetch("SENDKIT_API_KEY")
config.action_mailer.delivery_method = :sendkitNow every mailer in your app sends through Sendkit's email API without changing a single mailer class:
class UserMailer < ApplicationMailer
def welcome(user)
@user = user
mail(
to: @user.email,
subject: "Welcome to the platform"
)
end
end
# Call it as usual
UserMailer.welcome(@user).deliver_nowYour existing mailer tests, previews, and interceptors all keep working. The only thing that changed is the wire protocol underneath.
Background jobs with Active Job and Sidekiq
Never send email inside a web request if you can avoid it. Network calls to any external API add latency and introduce failure modes your users shouldn't see.
Rails makes this trivial with deliver_later:
UserMailer.welcome(@user).deliver_laterThat's it. Active Job picks it up, and your configured queue adapter (Sidekiq, Solid Queue, Good Job) handles the rest. Rails automatically retries failed jobs, so transient Sendkit errors get handled for free.
If you're using the SDK directly instead of Action Mailer, wrap it in an Active Job:
class SendEmailJob < ApplicationJob
queue_as :mailers
retry_on Sendkit::Error, wait: :polynomially_longer, attempts: 5
def perform(to:, subject:, html:)
SENDKIT_CLIENT.emails.send(
from: "[email protected]",
to: to,
subject: subject,
html: html
)
end
end
# Enqueue from anywhere
SendEmailJob.perform_later(
to: "[email protected]",
subject: "Your report is ready",
html: "<p>Download it from your dashboard.</p>"
)retry_on Sendkit::Error catches both rate limits and transient failures. The polynomially_longer wait strategy spaces retries out so you don't hammer the API after a 429.
For Sidekiq specifically, you can also use native Sidekiq jobs with more granular retry control:
class SendEmailWorker
include Sidekiq::Job
sidekiq_options retry: 5, queue: "mailers"
def perform(to, subject, html)
SENDKIT_CLIENT.emails.send(
from: "[email protected]",
to: to,
subject: subject,
html: html
)
rescue Sendkit::Error => e
raise if e.status_code != 429
self.class.perform_in(30, to, subject, html)
end
endError handling with Sendkit::Error
The SDK raises Sendkit::Error on any non-2xx response. The error carries name, message, and status_code attributes.
begin
SENDKIT_CLIENT.emails.send(
from: "[email protected]",
to: params[:email],
subject: "Password reset",
html: render_to_string(partial: "password_reset")
)
rescue Sendkit::Error => e
Rails.logger.error("[Sendkit] #{e.status_code} #{e.name}: #{e.message}")
case e.status_code
when 422
# Bad input: invalid from domain, missing fields
Sentry.capture_exception(e)
when 401
# API key is wrong or expired
Sentry.capture_exception(e, level: :fatal)
when 429
# Rate limited, retry later
SendEmailJob.set(wait: 1.minute).perform_later(to: params[:email], ...)
end
endCommon status codes: 422 for validation errors, 401 for bad keys, 429 for rate limits. Check Sendkit docs for the full list. Setting up proper email deliverability practices will help you avoid most 422 errors related to domain verification.

Webhook endpoint
Sendkit fires webhooks for deliveries, bounces, complaints, opens, and clicks. You need a Rails controller to receive them:
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token, only: :sendkit
def sendkit
payload = JSON.parse(request.body.read)
event_type = payload["type"]
case event_type
when "email.delivered"
Rails.logger.info("Delivered: #{payload['data']['email_id']}")
when "email.bounced"
email = payload["data"]["to"]
User.where(email: email).update_all(email_bounced: true)
Rails.logger.warn("Bounce: #{email}")
when "email.complained"
email = payload["data"]["to"]
User.where(email: email).update_all(email_opted_out: true)
end
head :ok
end
endAdd the route:
# config/routes.rb
post "/webhooks/sendkit", to: "webhooks#sendkit"In production, verify the webhook signature to make sure the request actually came from Sendkit. The docs cover signature verification. Process webhooks in a background job if the handler does anything slow -- the endpoint should return 200 quickly so Sendkit doesn't retry.
SMTP alternative
If you'd rather not write a custom delivery method or you're migrating from another provider, Sendkit works as a standard SMTP relay. Configure Action Mailer with SMTP settings:
# config/environments/production.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: "smtp.sendkit.com",
port: 587,
user_name: "sendkit",
password: ENV.fetch("SENDKIT_API_KEY"),
authentication: :plain,
enable_starttls_auto: true,
domain: "yourapp.com"
}Your API key is the SMTP password. No separate credentials. You get the same tracking (opens, clicks, bounces) as the API. The tradeoff: SMTP is fire-and-forget at the protocol level, so you don't get a message ID back synchronously. If you need confirmation of acceptance per message, use the API.
SMTP is the fastest migration path. Swap the settings, deploy, and every existing mailer just works.
What to read next
- Sendkit docs for the full API reference
- Email API overview for feature details
- SMTP service if SMTP fits your workflow better
- Pricing to pick the right plan
- How to send email with Ruby for non-Rails Ruby apps
- Improve email deliverability to keep your sender reputation healthy
- Ruby SDK on GitHub for source code and issues
The API approach gives you message IDs, structured errors, and tighter control. SMTP gives you zero-code migration. Pick whichever fits your situation, or use both -- Sendkit doesn't care which door you walk through.
Share this article