Vanessa LozzardoHow to send email with Elixir using an API or SMTP
Send email from Elixir apps using the Sendkit SDK. Covers setup, HTML email, error handling, SMTP, and production tips.

Elixir has gen_smtp and :ssl for sending email, but wiring them together means writing MIME construction code, managing connection pools, and handling retry logic that has nothing to do with your actual app. All that just to send a password reset.
An email API skips the plumbing. This guide walks through sending email from Elixir with the Sendkit email API: basic sends, HTML messages, error handling, and SMTP as a fallback option.
Install the SDK
Add sendkit to your dependencies in mix.exs:
def deps do
[
{:sendkit, "~> 1.0"}
]
endThen fetch it:
mix deps.getGrab an API key from your Sendkit dashboard. Keys starting with sk_live_ send real email. Keys starting with sk_test_ run the same validation pipeline but don't deliver anything, which is useful for tests.
Configure the client
Create a client with your API key:
client = Sendkit.new("sk_live_your_api_key")Don't hardcode the key. Read it from the environment instead:
client = Sendkit.new()This pulls from the SENDKIT_API_KEY environment variable. In a Phoenix app, you'd typically set this in config/runtime.exs or your deployment's environment.
Send a plain text email
The simplest send:
{:ok, %{"id" => id}} =
Sendkit.Emails.send(client, %{
from: "[email protected]",
to: ["[email protected]"],
subject: "Your order has shipped",
text: "Order #4821 shipped via FedEx. Tracking number: 7489201384."
})
IO.puts("Sent: #{id}")The from address must be on a domain you've verified in Sendkit. If you haven't set that up, the DMARC, DKIM, and SPF guide walks through the DNS records.
The to field takes a list of email address strings.
Send HTML email
Most transactional email needs formatting. Pass both text and html so clients that can't render HTML still display something useful:
defmodule MyApp.Email do
def send_welcome(user_email, user_name) do
client = Sendkit.new()
Sendkit.Emails.send(client, %{
from: "[email protected]",
to: [user_email],
subject: "Welcome, #{user_name}",
text: "Hey #{user_name}, thanks for signing up. Your account is ready.",
html: """
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<h2>Welcome, #{user_name}</h2>
<p>Your account is set up and ready to go.</p>
<p>A few things to try first:</p>
<ul>
<li>Complete your profile</li>
<li>Connect your first integration</li>
</ul>
<a href="https://app.yoursite.com/getting-started"
style="background: #0066ff; color: white; padding: 12px 24px;
text-decoration: none; border-radius: 4px;
display: inline-block; margin-top: 16px;">
Get started
</a>
</div>
"""
})
end
endUse inline CSS. Email clients strip <style> blocks and ignore external stylesheets, so inline is the only reliable option across Gmail, Outlook, and Apple Mail.

Wrapping it in a GenServer
In a real Elixir app, you probably don't want to block your request process on an HTTP call to an email API. A simple GenServer gives you async sending with built-in retries:
defmodule MyApp.Mailer do
use GenServer
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def send_async(params) do
GenServer.cast(__MODULE__, {:send, params})
end
@impl true
def init(_opts) do
{:ok, %{client: Sendkit.new()}}
end
@impl true
def handle_cast({:send, params}, %{client: client} = state) do
case Sendkit.Emails.send(client, params) do
{:ok, %{"id" => id}} ->
IO.puts("Email sent: #{id}")
{:error, %Sendkit.Error{status_code: 429}} ->
Process.send_after(self(), {:retry, params}, 60_000)
{:error, %Sendkit.Error{name: name, message: message}} ->
IO.puts("Failed to send: #{name} - #{message}")
end
{:noreply, state}
end
@impl true
def handle_info({:retry, params}, state) do
handle_cast({:send, params}, state)
{:noreply, state}
end
endAdd it to your supervision tree and call MyApp.Mailer.send_async/1 from your controllers or context modules. The email goes out in a separate process, so the user's request doesn't wait for the API round trip.
Error handling
The SDK returns {:ok, result} or {:error, %Sendkit.Error{}} tuples, so you can pattern match on them like anything else in Elixir. Here's how to handle the common failure modes:
def send_email_safely(client, params) do
case Sendkit.Emails.send(client, params) do
{:ok, %{"id" => id}} ->
{:ok, id}
{:error, %Sendkit.Error{status_code: 422, message: message}} ->
# Bad input: invalid address, missing required field
IO.puts("Validation error: #{message}")
{:error, :invalid_params}
{:error, %Sendkit.Error{status_code: 429, message: message}} ->
# Rate limited - back off and retry
IO.puts("Rate limited: #{message}")
{:error, :rate_limited}
{:error, %Sendkit.Error{status_code: code}} when code >= 500 ->
# Server-side failure - safe to retry
IO.puts("Server error: #{code}")
{:error, :server_error}
{:error, %Sendkit.Error{name: name, message: message}} ->
IO.puts("API error (#{name}): #{message}")
{:error, :api_error}
end
endFor production systems, add retry logic with exponential backoff:
def send_with_retry(client, params, attempts \\ 3)
def send_with_retry(_client, _params, 0) do
{:error, :max_retries_exceeded}
end
def send_with_retry(client, params, attempts) do
case send_email_safely(client, params) do
{:ok, id} ->
{:ok, id}
{:error, reason} when reason in [:rate_limited, :server_error] ->
delay = :math.pow(2, 4 - attempts) |> round() |> :timer.seconds()
Process.sleep(delay)
send_with_retry(client, params, attempts - 1)
{:error, reason} ->
{:error, reason}
end
endOnly retry on transient failures. A 422 means the request itself is wrong, and sending it again won't help.

Sending raw MIME messages
If you already have a MIME-encoded message (from Swoosh, Bamboo, or another library), you can send it directly:
{:ok, %{"id" => id}} =
Sendkit.Emails.send_mime(client, %{
envelope_from: "[email protected]",
envelope_to: "[email protected]",
raw_message: mime_string
})This is handy when migrating from another provider. You keep your existing email construction logic and just swap the transport layer.
Using SMTP instead of the API
If you're using Swoosh or Bamboo and don't want to write a custom adapter, Sendkit also provides an SMTP relay. Point your existing SMTP config at Sendkit's servers:
# config/config.exs (Swoosh example)
config :my_app, MyApp.Mailer,
adapter: Swoosh.Adapters.SMTP,
relay: "smtp.sendkit.com",
port: 587,
username: "sendkit",
password: "sk_live_your_api_key",
tls: :if_available,
auth: :alwaysYour API key works as the SMTP password. No separate credentials.
The API is still the better choice for new projects. There's no SMTP handshake overhead, you get a message ID back immediately, and errors come as structured data instead of SMTP reply codes. But SMTP works well when you need to plug into existing infrastructure.
Deliverability matters more than the send call
Getting the send/2 call working is the quick part. Whether the email lands in the inbox is a different problem entirely.
First, authenticate your domain. DKIM, SPF, and DMARC are non-negotiable at this point. Gmail and Outlook will either spam-folder or outright reject unauthenticated messages. The DMARC/DKIM/SPF guide covers the DNS records you need.
Second, pay attention to bounces. If you keep sending to addresses that don't exist, your sender reputation takes a hit, and that drags down delivery for every email you send. Sendkit auto-suppresses bounced addresses, but you should monitor your bounce rate in your own logs too.
Third, validate addresses before sending. If you accept user input for email addresses, run a validation call first. It catches typos like gmial.com and disposable addresses that will never engage. Sendkit has built-in validation for this, and there's a longer guide on validating email addresses before sending.
The Sendkit docs have more on webhooks, templates, and batch sending.
That's it
Add sendkit to your mix.exs, call Sendkit.Emails.send/2, handle the error tuples. Working email.
The code is the easy part. Deliverability is the ongoing work: authenticate your domain, don't ignore bounces, validate addresses. A few lines of Elixir get the email out. Keeping your sender reputation intact is where the real effort goes.
Share this article