← Back to all posts
2026-08-28 · MailAnvil Team

Kirim Email Transaksional dengan Ruby on Rails + MailAnvil

Rails masih jadi tulang punggung banyak startup dan fintech di Indonesia — marketplace, platform booking, sampai internal tool perusahaan. Umurnya panjang, ekosistem gempya, dan developer-nya banyak.

Tapi ada satu hal yang masih bikin salah langkah: kirim email transaksional. OTP, invoice, konfirmasi booking, reset password — semua email ini harus sampai cepat dan aman, bukan nyangkut di spam.

Kebanyakan developer Rails langsung pake ActionMailer + SMTP SendGrid/SES. Masalahnya: butuh credential SMTP eksternal, harga USD kena markup kurs 12-35%, dan bayar pakai kartu kredit internasional yang belum tentu kamu punya.

Tutorial ini kasih cara paling bersih kirim email dari Rails — pakai MailAnvil REST API lewat satu service class + ActiveJob, tanpa gem tambahan.

Kenapa MailAnvil?

Prasyarat

Step 1: Simpan API Key di Rails Credentials

Jangan hardcode API key. Rails punya credentials yang terenkripsi:

EDITOR="code --wait" bin/rails credentials:edit

Tambah di file yang kebuka:

mailanvil_api_key: re_xxxxxxxxxxxxxxxxxxxx

Akses dari kode pakai Rails.application.credentials.mailanvil_api_key.

Step 2: Service Class Reusable (Net::HTTP — stdlib)

Bikin app/services/mailanvil_client.rb. Net::HTTP udah bawaan Ruby, gak perlu gem tambahan:

# app/services/mailanvil_client.rb
require "net/http"
require "json"

class MailAnvilClient
  API_BASE = "https://api.mailanvil.com/v1"

  class Error < StandardError; end

  def send_email(to:, subject:, from:, html: nil, text: nil, idempotency_key: nil)
    raise Error, "missing mailanvil_api_key" if api_key.blank?

    uri  = URI("#{API_BASE}/send")
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl      = true
    http.open_timeout = 5
    http.read_timeout = 15

    request = Net::HTTP::Post.new(uri)
    request["Authorization"]   = "Bearer #{api_key}"
    request["Content-Type"]    = "application/json"
    request["Idempotency-Key"] = idempotency_key if idempotency_key

    request.body = {
      from:    from,
      to:      Array(to),
      subject: subject,
      html:    html,
      text:    text
    }.compact.to_json

    response = http.request(request)

    unless response.code == "202"
      body = JSON.parse(response.body) rescue {}
      err  = body["error"] || {}
      raise Error, "MailAnvil API #{response.code}: #{err["code"]} — #{err["message"] || response.body}"
    end

    JSON.parse(response.body)
  end

  private

  def api_key
    Rails.application.credentials.mailanvil_api_key
  end
end

Satu class, satu tanggung jawab, dipakai di seluruh app.

Step 3: Kirim via ActiveJob (Non-Blocking)

Jangan blocking HTTP response. Bungkus di ActiveJob:

# app/jobs/email_job.rb
class EmailJob < ApplicationJob
  queue_as :mailers

  def perform(to:, subject:, from:, html: nil, text: nil, idempotency_key: nil)
    MailAnvilClient.new.send_email(
      to: to, subject: subject, from: from,
      html: html, text: text, idempotency_key: idempotency_key
    )
  end
end

Step 4: Contoh Nyata — Konfirmasi Order

Kirim email konfirmasi order saat transaksi sukses:

# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
  def create
    order = Order.create!(order_params)

    EmailJob.perform_later(
      to:      order.customer.email,
      from:    "MailAnvil <[email protected]>",
      subject: "Order ##{order.id} dikonfirmasi",
      html:    render_to_string("orders/confirmation", locals: { order: order }, layout: "mailer"),
      idempotency_key: "order-#{order.id}"  # aman retry tanpa email duplikat
    )

    render json: { status: "ok", order_id: order.id }, status: :created
  end
end

perform_later naruh job ke queue — response balik cepet, email dikirim di background. idempotency_key pastikan kalau job ke-retry, email gak terkirim dua kali.

Step 5: Template Email

app/views/orders/confirmation.html.erb:

<!DOCTYPE html>
<html>
<body style="font-family: system-ui, sans-serif; max-width: 600px; margin: 0 auto; padding: 40px 20px; background: #000; color: #fff;">
  <h1 style="color: #ff801f;">Order Dikonfirmasi</h1>
  <p>Hai <%= order.customer.name %>, order kamu <strong>#<%= order.id %></strong> udah kami terima.</p>
  <p style="font-size: 24px; font-weight: 700; color: #ff801f;">Rp <%= number_with_delimiter(order.total) %></p>
  <p style="font-size: 14px; color: #666;">Simpan email ini sebagai bukti transaksi.</p>
</body>
</html>

Production Tips

1. Idempotency Key untuk Safe Retry

Kalau job ke-retry (worker crash, timeout), idempotency key bikin email yang sama gak terkirim dua kali. User gak bakal dapat dua email identik yang bikin bingung.

2. Queue Backend

perform_later butuh backend queue. Development: :async (bawaan). Production: Solid Queue (Rails 8 bawaan) atau Sidekiq.

3. Timeout & Retry

Net::HTTP di atas udah set open_timeout 5 detik dan read_timeout 15 detik. Tambah retry_on di job buat transient error:

class EmailJob < ApplicationJob
  retry_on MailAnvilClient::Error, wait: :exponentially_longer, attempts: 3
  # ...
end

4. Jangan Taruh Secret di Repo

Rails.application.credentials udah terenkripsi (config/credentials.yml.enc). File ini aman di-commit. Jangan pernah commit API key plaintext ke repo publik.

Perbandingan dengan Approach Lain

Approach Dependency Blocking Retry Harga IDR
ActionMailer + SMTP SendGrid credential SMTP Tidak Manual USD + kurs
SendGrid/SES SDK 1 gem + akun Tidak Built-in USD + kurs
Resend SDK 1 gem + akun Tidak Built-in USD + kurs
MailAnvil REST 0 gem (Net::HTTP) Tidak (ActiveJob) Idempotency key IDR + QRIS

Tanpa gem tambahan — Net::HTTP stdlib udah cukup. Satu service class, satu job, jalan.

Kesimpulan

Rails + MailAnvil = kombinasi simpel dan aman buat email transaksional. Kamu dapet:

Coba sendiri: daftar di mailanvil.com, verifikasi domain, dan jalankan contoh kode di atas dalam 5 menit.


Butuh API key? Daftar early access di mailanvil.com — gratis 500 email/bulan.