Framework guides
Send email from Ruby on Rails
Send transactional email from Rails with a plain HTTP call — as a service object and as a background ActiveJob.
Rails ships Net::HTTP in the standard library, so a transactional send
needs no extra gem. This is the same POST /api/v1/emails endpoint
documented in Send a transactional email.
Service object#
# app/services/bytloop_mail_client.rb
require "net/http"
require "json"
class BytloopMailClient
ENDPOINT = URI("https://mail.bytloop.com/api/v1/emails")
class SendError < StandardError; end
def self.send_email(from:, to:, subject:, html:, idempotency_key:, tags: {})
request = Net::HTTP::Post.new(ENDPOINT)
request["Authorization"] = "Bearer #{Rails.application.credentials.bytloop_api_key}"
request["Content-Type"] = "application/json"
request["Idempotency-Key"] = idempotency_key
request.body = {
from: from,
to: Array(to),
subject: subject,
html: html,
tags: tags,
}.to_json
response = Net::HTTP.start(ENDPOINT.hostname, ENDPOINT.port, use_ssl: true) do |http|
http.request(request)
end
unless response.is_a?(Net::HTTPSuccess)
raise SendError, "Bytloop Mail send failed (#{response.code}): #{response.body}"
end
JSON.parse(response.body)
end
endCall it directly for a one-off send:
BytloopMailClient.send_email(
from: "Support <support@send.acme.com>",
to: order.customer_email,
subject: "Your receipt for order ##{order.id}",
html: render_to_string(partial: "receipts/email", locals: { order: order }),
idempotency_key: "receipt-#{order.id}",
tags: { receipt_for_order: order.id.to_s },
)Background job (recommended)#
A synchronous Net::HTTP call inside a controller action blocks the
request on the API round-trip. Wrap it in an ActiveJob — same pattern
you'd use for ActionMailer#deliver_later — so the response comes back
immediately and a transient failure retries instead of failing the
request:
# app/jobs/send_receipt_email_job.rb
class SendReceiptEmailJob < ApplicationJob
queue_as :default
retry_on BytloopMailClient::SendError, wait: :polynomially_longer, attempts: 5
def perform(order_id)
order = Order.find(order_id)
BytloopMailClient.send_email(
from: "Support <support@send.acme.com>",
to: order.customer_email,
subject: "Your receipt for order ##{order.id}",
html: render_to_string(partial: "receipts/email", locals: { order: order }),
idempotency_key: "receipt-#{order.id}",
tags: { receipt_for_order: order.id.to_s },
)
end
end# wherever the order is confirmed
SendReceiptEmailJob.perform_later(order.id)Credentials#
# rails credentials:edit
bytloop_api_key: bm_live_xxxxxxxxxxxxxxxxPlain ENV["BYTLOOP_API_KEY"] via dotenv-rails works identically if
your app isn't on Rails credentials.
Common pitfalls#
- Calling it synchronously from a controller. Blocks the request on
the API round-trip for no reason the user can see. Use the
ActiveJobpattern above for anything triggered by a user action. - No
retry_on. Without it, a transient network error or a5xxfrom a deploy in progress silently drops the email.retry_onwith polynomial backoff (shown above) is the same shape Rails already uses forActionMailer. - Reusing the idempotency key across retries with different content. The key must stay tied to what is being sent (the order id), not the job attempt — that's what makes a retried job safe to re-run.
Next#
- Register a webhook endpoint (a Rails controller action) to hear about delivery, bounces, and opens.
- Read the rate limits page so a bulk job (e.g.
Order.where(...).find_each) doesn't get throttled mid-run.