Bytloop MailDocs
Status

Getting started

  • Introduction
  • Quickstart

Sending email

  • Send a transactional email
  • Sending domains vs mailboxes

Framework guides

  • Send email from Next.js
  • Send email from Ruby on Rails
  • Send email from Django
  • Send email from Laravel

Domains

  • Verify a domain

Receiving email

  • Receive email
  • Webhooks

Reference

  • Rate limits
  • Errors
  • SDKs

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.

Try it live

Every endpoint in this doc runs against your workspace from the interactive API explorer. Prefill a request, hit Send, and see the response with your own keys.

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
end
# 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
end

Call 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 },
)
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
# 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)
# wherever the order is confirmed
SendReceiptEmailJob.perform_later(order.id)

Credentials#

# rails credentials:edit
bytloop_api_key: bm_live_xxxxxxxxxxxxxxxx
# rails credentials:edit
bytloop_api_key: bm_live_xxxxxxxxxxxxxxxx

Plain 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 ActiveJob pattern above for anything triggered by a user action.
  • No retry_on. Without it, a transient network error or a 5xx from a deploy in progress silently drops the email. retry_on with polynomial backoff (shown above) is the same shape Rails already uses for ActionMailer.
  • 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.
PreviousSend email from Next.jsNextSend email from Django

On this page

  • Service object
  • Background job (recommended)
  • Credentials
  • Common pitfalls
  • Next