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

Nik@nikpolale
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
]
endmix.exs
defp deps do
[
{:phoenix, "~> 1.7"},
{:swoosh, "~> 1.16"},
# Swoosh has a built-in Resend adapter
]
endmix.exs
defp deps do
[
{:phoenix, "~> 1.7"},
{:swoosh, "~> 1.16"},
# Swoosh has a built-in SendGrid adapter
]
endCreate 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
endlib/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
endlib/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
endlib/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
endBackground 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
- Swoosh for provider-agnostic email composition
- Direct API calls with Req for providers without Swoosh adapters
- Phoenix controllers for route-based sending
- Oban for reliable background jobs
Pick your provider, copy the patterns, and start sending.