Back to Blog

How to Send Emails in Elixir / Phoenix (2026 Guide)

12 min read

Elixir has Swoosh for email sending. It provides a unified API with adapters for SMTP and API-based providers. Phoenix generators include Swoosh by default.

For API-based providers without Swoosh adapters, you can use Req or HTTPoison directly. This guide covers both approaches.

Swoosh vs Direct API

# Swoosh: unified API, adapter handles transport
import Swoosh.Email
 
new()
|> to("user@example.com")
|> from({"Your App", "noreply@yourdomain.com"})
|> subject("Hello")
|> html_body("<h1>Welcome</h1>")
|> MyApp.Mailer.deliver()
 
# Direct API: one HTTP call
Req.post!("https://api.sequenzy.com/v1/transactional/send",
  headers: [{"authorization", "Bearer #{api_key}"}],
  json: %{to: "user@example.com", subject: "Hello", body: "<h1>Welcome</h1>"}
)

Install

Add to your mix.exs:

mix.exs
defp deps do
[
  {:phoenix, "~> 1.7"},
  {:req, "~> 0.5"},
  # or use {:swoosh, "~> 1.16"} with a custom adapter
]
end
mix.exs
defp deps do
[
  {:phoenix, "~> 1.7"},
  {:swoosh, "~> 1.16"},
  # Swoosh has a built-in Resend adapter
]
end
mix.exs
defp deps do
[
  {:phoenix, "~> 1.7"},
  {:swoosh, "~> 1.16"},
  # Swoosh has a built-in SendGrid adapter
]
end

Create an Email Module

lib/my_app/email.ex
defmodule MyApp.Email do
@api_key System.get_env("SEQUENZY_API_KEY")
@base_url "https://api.sequenzy.com/v1"

def send_email(to, subject, body) do
  Req.post!("#{@base_url}/transactional/send",
    headers: [{"authorization", "Bearer #{@api_key}"}],
    json: %{to: to, subject: subject, body: body}
  )
end

def welcome(email, name) do
  send_email(
    email,
    "Welcome, #{name}",
    """
    <h1>Welcome, #{name}</h1>
    <p>Your account is ready.</p>
    <a href="#{MyAppWeb.Endpoint.url()}/dashboard"
       style="display:inline-block;background:#f97316;color:#fff;padding:12px 24px;border-radius:6px;text-decoration:none;">
      Go to Dashboard
    </a>
    """
  )
end
end
lib/my_app/email.ex
defmodule MyApp.Email do
import Swoosh.Email

def welcome(email, name) do
  new()
  |> to(email)
  |> from({"Your App", "noreply@yourdomain.com"})
  |> subject("Welcome, #{name}")
  |> html_body("""
  <h1>Welcome, #{name}</h1>
  <p>Your account is ready.</p>
  """)
end
end

# config/runtime.exs
config :my_app, MyApp.Mailer,
adapter: Swoosh.Adapters.Resend,
api_key: System.get_env("RESEND_API_KEY")
lib/my_app/email.ex
defmodule MyApp.Email do
import Swoosh.Email

def welcome(email, name) do
  new()
  |> to(email)
  |> from({"Your App", "noreply@yourdomain.com"})
  |> subject("Welcome, #{name}")
  |> html_body("""
  <h1>Welcome, #{name}</h1>
  <p>Your account is ready.</p>
  """)
end
end

# config/runtime.exs
config :my_app, MyApp.Mailer,
adapter: Swoosh.Adapters.Sendgrid,
api_key: System.get_env("SENDGRID_API_KEY")

Send from a Controller

lib/my_app_web/controllers/email_controller.ex
defmodule MyAppWeb.EmailController do
use MyAppWeb, :controller

def send_welcome(conn, %{"email" => email, "name" => name}) do
  case MyApp.Email.welcome(email, name) do
    %Req.Response{status: 200, body: body} ->
      json(conn, %{jobId: body["jobId"]})

    _ ->
      conn
      |> put_status(500)
      |> json(%{error: "Failed to send"})
  end
end
end
lib/my_app_web/controllers/email_controller.ex
defmodule MyAppWeb.EmailController do
use MyAppWeb, :controller

def send_welcome(conn, %{"email" => email, "name" => name}) do
  email = MyApp.Email.welcome(email, name)

  case MyApp.Mailer.deliver(email) do
    {:ok, _meta} ->
      json(conn, %{sent: true})

    {:error, reason} ->
      conn
      |> put_status(500)
      |> json(%{error: "Failed to send"})
  end
end
end
lib/my_app_web/controllers/email_controller.ex
defmodule MyAppWeb.EmailController do
use MyAppWeb, :controller

def send_welcome(conn, %{"email" => email, "name" => name}) do
  email = MyApp.Email.welcome(email, name)

  case MyApp.Mailer.deliver(email) do
    {:ok, _meta} ->
      json(conn, %{sent: true})

    {:error, reason} ->
      conn
      |> put_status(500)
      |> json(%{error: "Failed to send"})
  end
end
end

Background Sending with Oban

# mix.exs
{:oban, "~> 2.17"}
 
# lib/my_app/workers/email_worker.ex
defmodule MyApp.Workers.EmailWorker do
  use Oban.Worker, queue: :emails
 
  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"to" => to, "subject" => subject, "body" => body}}) do
    MyApp.Email.send_email(to, subject, body)
    :ok
  end
end
 
# Usage - queue an email
%{to: email, subject: "Welcome", body: "<h1>Welcome!</h1>"}
|> MyApp.Workers.EmailWorker.new()
|> Oban.insert()

Going to Production

1. Verify Your Domain

Add SPF, DKIM, DMARC DNS records.

2. Use Runtime Config

# config/runtime.exs
config :my_app, :sequenzy_api_key, System.get_env("SEQUENZY_API_KEY")

3. Use Oban for Reliability

Oban persists jobs in PostgreSQL. Emails survive restarts and retries are automatic.

Beyond Transactional

Sequenzy handles transactional sends, marketing campaigns, automated sequences, and subscriber management from one API. Native Stripe integration for SaaS.

Wrapping Up

  1. Swoosh for provider-agnostic email composition
  2. Direct API calls with Req for providers without Swoosh adapters
  3. Phoenix controllers for route-based sending
  4. Oban for reliable background jobs

Pick your provider, copy the patterns, and start sending.