Paulo CastellanoHow to send email with Ruby using an email API or SMTP
Send email from Ruby apps using the Sendkit SDK. Covers gem setup, HTML email, MIME sending, error handling, and SMTP relay.

Ruby's Net::SMTP has been around forever, and it shows. You end up manually building MIME parts, fiddling with Content-Type headers, and writing more email plumbing than actual application logic. ActionMailer abstracts some of this away in Rails, but if you're outside Rails or want direct control over delivery tracking, you're back to square one.
An email API is simpler. You send a hash of parameters, get back a response, and move on. This guide walks through sending email from Ruby using the Sendkit email API, starting with basic sends and working up to MIME messages, error handling, and SMTP as a fallback.

Install the gem
Add it to your Gemfile:
gem "sendkit"Then run bundle install. Or install it directly:
gem install sendkitYou'll need an API key from your Sendkit dashboard. Keys prefixed with sk_live_ send real email. Keys prefixed with sk_test_ validate your request but don't deliver anything, which is useful for test suites.
Initialize the client
Two options. Pass the key directly:
require "sendkit"
client = Sendkit::Client.new("sk_live_your_api_key")Or set the SENDKIT_API_KEY environment variable and let the client pick it up:
client = Sendkit::Client.newI prefer the env var approach. It keeps keys out of version control and makes it easy to swap between test and live keys per environment.

Send a plain text email
The most basic send:
result = client.emails.send(
from: "[email protected]",
to: "[email protected]",
subject: "Your order has shipped",
text: "Order #4821 shipped via FedEx. Tracking number: 7489201384."
)
puts result["id"]result["id"] is the message ID. Hold onto it if you need to look up delivery status later or correlate with webhook events.
The from address must be on a domain you've verified in Sendkit. If you try to send from an unverified domain, you'll get a validation error back.
Send an HTML email
Most transactional email needs HTML. Pass an html parameter alongside (or instead of) text:
result = client.emails.send(
from: "[email protected]",
to: "[email protected]",
subject: "Invoice #1042",
html: <<~HTML
<h1>Invoice #1042</h1>
<p>Amount due: <strong>$249.00</strong></p>
<p>Due date: March 15, 2026</p>
<p><a href="https://yourapp.com/invoices/1042">View invoice</a></p>
HTML
)If you include both text and html, email clients that can't render HTML will fall back to the plain text version. Worth doing for accessibility, even though it's rare in practice.
Send to multiple recipients
Pass an array for to:
result = client.emails.send(
from: "[email protected]",
to: ["[email protected]", "[email protected]"],
subject: "Weekly report",
html: "<p>This week's metrics are attached.</p>"
)Each recipient gets their own copy. This isn't BCC. Each address appears in the To header of the message they receive.
Reply-to and custom headers
Set a reply_to when you want responses going somewhere other than the from address:
result = client.emails.send(
from: "[email protected]",
to: "[email protected]",
reply_to: "[email protected]",
subject: "Your account has been created",
html: "<p>Welcome aboard. Reply to this email if you have any questions.</p>"
)This is common for transactional email where the sending address is a noreply@ but you still want replies to land in a real inbox.
Error handling
The SDK raises Sendkit::Error when something goes wrong. The error object has three useful attributes: name, message, and status_code.
begin
result = client.emails.send(
from: "[email protected]",
to: "[email protected]",
subject: "Test",
html: "<p>Hello</p>"
)
puts result["id"]
rescue Sendkit::Error => e
puts e.name # "validation_error"
puts e.message # "The from domain is not verified."
puts e.status_code # 422
end
Common errors you'll hit:
- 422 with
validation_error: Missing or invalid fields. Usually a badfromdomain or missingto. - 401 with
authentication_error: Invalid API key. Double-check you're using the right key for the environment. - 429 with
rate_limit_error: Too many requests. The response includes aRetry-Afterheader telling you how long to wait. Sendkit allows 60 to 1,200 requests per minute depending on your plan.
For production code, catch rate limits specifically and retry with backoff:
def send_with_retry(client, params, max_retries: 3)
retries = 0
begin
client.emails.send(**params)
rescue Sendkit::Error => e
if e.status_code == 429 && retries < max_retries
retries += 1
sleep(retries * 2)
retry
end
raise
end
endSend raw MIME messages
If you already have a MIME-formatted message (from Mail gem, or because you're migrating from another provider), use send_mime:
require "mail"
mime = Mail.new do
from "[email protected]"
to "[email protected]"
subject "Monthly statement"
text_part do
body "Your statement is attached."
end
html_part do
content_type "text/html; charset=UTF-8"
body "<p>Your statement is attached.</p>"
end
add_file "/path/to/statement.pdf"
end
client.emails.send_mime(
envelope_from: "[email protected]",
envelope_to: "[email protected]",
raw_message: mime.to_s
)This is the escape hatch for anything the standard send method doesn't cover: inline images, S/MIME signing, calendar invitations, or multipart messages with unusual structures.
Using SMTP instead
If you'd rather use SMTP, Sendkit works as a drop-in SMTP relay. This is handy if you're already using ActionMailer or another library that speaks SMTP.
Configure your SMTP settings:
require "net/smtp"
smtp = Net::SMTP.new("smtp.sendkit.dev", 587)
smtp.enable_starttls
smtp.start("yourdomain.com", "sendkit", "sk_live_your_api_key", :plain) do |s|
s.send_message(
"From: [email protected]\r\n" \
"To: [email protected]\r\n" \
"Subject: Test\r\n" \
"\r\n" \
"Hello from SMTP.",
"[email protected]",
"[email protected]"
)
endYour API key is the SMTP password. No separate credentials to manage.
For Rails apps, update config/environments/production.rb:
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: "smtp.sendkit.dev",
port: 587,
user_name: "sendkit",
password: ENV["SENDKIT_API_KEY"],
authentication: :plain,
enable_starttls_auto: true,
domain: "yourapp.com"
}SMTP gives you the same tracking (opens, clicks, bounces) as the API. The difference is the API returns a message ID synchronously, while SMTP is fire-and-forget at the protocol level. If you need confirmation of acceptance, the API is more predictable.
Sending from a background job
Email sending shouldn't block your web requests. In a Rails app, push it to a Sidekiq job:
class SendEmailJob
include Sidekiq::Job
def perform(to, subject, html)
client = Sendkit::Client.new
client.emails.send(
from: "[email protected]",
to: to,
subject: subject,
html: html
)
rescue Sendkit::Error => e
if e.status_code == 429
# Re-enqueue with delay on rate limit
self.class.perform_in(30, to, subject, html)
else
raise
end
end
end
# Enqueue from your controller
SendEmailJob.perform_async("[email protected]", "Welcome!", "<p>Hello</p>")The Sendkit::Client.new call in the job reads from SENDKIT_API_KEY, so no secrets in your job arguments.
Putting it together
Here's a more realistic example. An order confirmation with error handling and logging:
require "sendkit"
require "logger"
logger = Logger.new($stdout)
client = Sendkit::Client.new
def send_order_confirmation(client, logger, order)
result = client.emails.send(
from: "[email protected]",
to: order[:email],
reply_to: "[email protected]",
subject: "Order ##{order[:id]} confirmed",
html: <<~HTML
<h1>Thanks for your order</h1>
<p>Order ##{order[:id]}</p>
<p>Total: $#{order[:total]}</p>
<p>We'll email you again when it ships.</p>
HTML
)
logger.info("Sent order confirmation #{result['id']} to #{order[:email]}")
result
rescue Sendkit::Error => e
logger.error("Failed to send order confirmation: #{e.name} - #{e.message}")
raise
end
send_order_confirmation(client, logger, {
id: 8832,
email: "[email protected]",
total: "149.00"
})What to read next
- Sendkit docs for the full API reference
- Email API overview for feature details and pricing
- SMTP service if you prefer SMTP over REST
- Ruby SDK on GitHub for source code and issues
The Ruby SDK covers the same surface as the REST API. If you can do it with a curl request, you can do it with the gem. The main advantage is typed errors and not having to manage HTTP headers yourself.
Share this article