--- title: "Send email from Phoenix with the native AhaSend adapter for Swoosh" description: "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:" url: https://ahasend.com/blog/send-email-phoenix-native-ahasend-adapter-swoosh markdown_url: https://ahasend.com/blog/send-email-phoenix-native-ahasend-adapter-swoosh.md lang: en type: blog_post published: 2026-10-28 updated: 2026-10-29 author: "Mark Kraakman" category: "Product Update" image: https://cms.ahasend.com/sites/default/files/2026-10/phoenix-swoosh_0.png --- # Send email from Phoenix with the native AhaSend adapter for Swoosh _By Mark Kraakman, October 28, 2026, in Product Update_ ![Send email from Phoenix with the native AhaSend adapter for Swoosh](https://cms.ahasend.com/sites/default/files/2026-10/phoenix-swoosh_0.png) 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: ```plaintext # 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](https://github.com/swoosh/swoosh/pull/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. ```plaintext defp deps do [ {:swoosh, "~> 1.27"}, {:req, "~> 0.5"} ] end ``` Tell 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`. ```plaintext # 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") end ``` In 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: ```plaintext defmodule MyApp.Mailer do use Swoosh.Mailer, otp_app: :my_app end ``` On 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: ```plaintext import Swoosh.Email new() |> to({user.name, user.email}) |> from({"MyApp", "noreply@mail.example.com"}) |> 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"}`. - `sandbox` and `sandbox_result`: accept and validate the message without delivering it, and simulate `deliver`, `bounce`, `defer`, `fail` or `suppress`. 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. [Start for free on AhaSend](https://dash.ahasend.com/user/register)