On this page
Swoosh now has a native AhaSend adapter. Until now, Elixir developers reached AhaSend over SMTP or wrote their own API client. Now it is a version bump and three lines of config:
# config/runtime.exs
config :my_app, MyApp.Mailer,
adapter: Swoosh.Adapters.AhaSend,
api_key: System.fetch_env!("AHASEND_API_KEY"),
account_id: System.fetch_env!("AHASEND_ACCOUNT_ID")Behind it you get the full Management API: tags, tracking and retention per message, scheduling, idempotency keys and a sandbox mode for integration tests.
What changed
Swoosh 1.27.0 added Swoosh.Adapters.AhaSend, contributed through pull request #1177. The adapter talks to the AhaSend v2 API directly, picks the right endpoint for the email you built, and maps Swoosh's provider options onto the API's message fields. Any Swoosh 1.27 or later includes it, so a fresh mix phx.new already has it.
Setup in a Phoenix project
A generated Phoenix project already depends on Swoosh; make sure it is at least 1.27. Swoosh also needs an HTTP client and works with Req, Finch or Hackney. Req is the simplest choice for a new project.
defp deps do
[
{:swoosh, "~> 1.27"},
{:req, "~> 0.5"}
]
endTell Swoosh which client to use, and point the mailer at AhaSend in production. Keep the key and account id out of the repository: read them from the environment in config/runtime.exs.
# config/config.exs
config :swoosh, :api_client, Swoosh.ApiClient.Req
# config/runtime.exs
if config_env() == :prod do
config :my_app, MyApp.Mailer,
adapter: Swoosh.Adapters.AhaSend,
api_key: System.fetch_env!("AHASEND_API_KEY"),
account_id: System.fetch_env!("AHASEND_ACCOUNT_ID")
endIn development, leave the Swoosh.Adapters.Local adapter that Phoenix generated, so emails land in the mailbox preview at /dev/mailbox instead of in a real inbox. In test, keep Swoosh.Adapters.Test; it is what makes assert_email_sent/1 work, and it needs no API key.
The mailer module is the one Phoenix already created:
defmodule MyApp.Mailer do
use Swoosh.Mailer, otp_app: :my_app
endOn the AhaSend side, create a scoped API key on the API Keys page of the dashboard with only the messages:send:{your-domain} scope (or messages:send:all if it may send from every domain), and add a sending domain with its DNS records verified. That is the whole setup.
Sending an email
Nothing changes in how you build messages. A password reset from the phx.gen.auth notifier looks like this:
import Swoosh.Email
new()
|> to({user.name, user.email})
|> from({"MyApp", "[email protected]"})
|> subject("Reset your password")
|> text_body("Use this link to reset your password: #{url}")
|> put_provider_option(:tags, ["auth", "password-reset"])
|> put_provider_option(:idempotency_key, "reset-#{token_id}")
|> MyApp.Mailer.deliver()The idempotency_key option is worth adopting from day one. It is sent as the Idempotency-Key header, so when an Oban job is retried after a timeout, the same email is not delivered twice.
What the adapter does for you
AhaSend has a /messages endpoint that delivers an individual copy to each recipient, and a /messages/conversation endpoint that sends one message carrying the full To, Cc and Bcc headers, the way a mail client does. The adapter uses the first when your email has no Cc or Bcc, and the second when it has, so recipients see exactly what you intended.
Provider options map onto the API. Besides tags and idempotency_key:
substitutions: a map of template variables applied to the subject and body.tracking: override the account default per message, for example%{open: true, click: false}.retention: override how long AhaSend keeps metadata and content, for example%{metadata: 30, data: 7}, within your plan's limits.schedule: defer delivery, for example%{first_attempt: "2026-11-01T08:00:00Z"}.sandboxandsandbox_result: accept and validate the message without delivering it, and simulatedeliver,bounce,defer,failorsuppress. This is for integration tests that should hit the real API without sending mail; unit tests stay on the Test adapter.
Why Phoenix teams pick AhaSend
Phoenix is popular with European teams that care where their data lives. AhaSend runs on bare metal it operates itself in European datacenters, stores your data in Germany and Finland, and is ISO 27001 certified. The free plan includes 1,000 emails a month, enough to ship an MVP without a credit card. Above that, Pro starts at €9 a month for 25,000 emails and Max at €72 for 100,000, with usage based overage on both.