Skip to content

Send emails with Elixir

Learn how to send transactional emails using PostStack and Elixir.

Elixir’s Req library wraps Finch and Mint under a friendlier surface — JSON encoding, decompression, retries, and telemetry are configured in keyword args. For Phoenix apps, Req plays nicely with the BEAM’s supervision: a transient failure during a send does not crash the user-facing process. For pure email-sending background work, Bamboo and Swoosh both ship SMTP adapters that work with PostStack out of the box.

1. Install the SDK

bash
# mix.exs
{:req, "~> 0.5"}

2. Initialize the client

elixir
# config/config.exs
config :my_app,
  poststack_api_key: System.get_env("POSTSTACK_API_KEY")

3. Send an email

elixir
defmodule MyApp.Email do
  @base_url "https://api.poststack.dev"

  def send_welcome(to) do
    api_key = Application.get_env(:my_app, :poststack_api_key)

    Req.post!("#{@base_url}/emails",
      json: %{
        from: "hello@yourdomain.com",
        to: [to],
        subject: "Hello from Elixir!",
        html: "<h1>Welcome!</h1>"
      },
      headers: [
        {"authorization", "Bearer #{api_key}"},
        {"content-type", "application/json"}
      ]
    )
  end
end

4. Handle errors

Elixir idioms for error handling, retries, and structured logging when calling the PostStack API.

elixir
defmodule MyApp.Email do
  @base_url "https://api.poststack.dev"

  def send_with_retry(payload, attempts \\ 3) do
    api_key = Application.fetch_env!(:my_app, :poststack_api_key)

    Req.post("#{@base_url}/emails",
      json: payload,
      headers: [
        {"authorization", "Bearer #{api_key}"},
        {"content-type", "application/json"}
      ],
      retry: :transient,
      max_retries: attempts,
      retry_delay: fn attempt -> :timer.seconds(2 ** attempt) end
    )
    |> case do
      {:ok, %Req.Response{status: status, body: body}} when status in 200..299 ->
        {:ok, body}

      {:ok, %Req.Response{status: status, body: body, headers: headers}} ->
        request_id = List.keyfind(headers, "x-request-id", 0)
        {:error, %{status: status, body: body, request_id: request_id}}

      {:error, exception} ->
        {:error, exception}
    end
  end
end

Framework integrations

Phoenix LiveView

Trigger sends from event handlers. Wrap `Task.start_link(fn -> MyApp.Email.send_with_retry(payload) end)` so the LiveView process is not blocked. The task supervisor restarts crashed sends without affecting the LiveView socket.

Swoosh / Bamboo with SMTP

For Phoenix mailer ergonomics, configure Swoosh or Bamboo with the `Swoosh.Adapters.SMTP` adapter pointing at `smtp.poststack.dev:587` with the API key as the password. Mailers, previews, and tests work unchanged.

Oban background jobs

Wrap email sends in an Oban worker so retries, deduplication, and rate limiting happen at the queue layer. Configure `max_attempts: 5` and Oban’s exponential backoff handles transient PostStack 5xx errors automatically.

GenStage / Broadway pipelines

For mass-fan-out (newsletter sends, batched notifications), feed payloads through a Broadway pipeline with a PostStack-sending processor. Backpressure keeps the pipeline within your API rate limit.

Common pitfalls

  • Blocking the LiveView process

    A synchronous `Req.post!` inside a LiveView event handler freezes the UI for the duration of the round trip. Always spawn the send into a Task or enqueue it in Oban.

  • Reading the API key at compile time

    `@api_key Application.compile_env(:my_app, :poststack_api_key)` bakes the key into the BEAM bytecode and ignores runtime env changes. Use `Application.fetch_env!/2` inside the function instead.

  • Using `Application.get_env` without a default

    Returns `nil` silently if the key is missing. Use `fetch_env!` (raises on missing) so misconfiguration crashes loudly rather than producing failed sends with cryptic `nil` headers.

Notes

  • Uses the Req HTTP client library

FAQ

Can I use Bamboo or Swoosh?

Yes — configure the SMTP adapter pointing at smtp.poststack.dev:587. Your existing mailer modules work unchanged.

What about Oban?

Oban is the recommended pattern for non-blocking sends. Wrap the call in an Oban worker, set `max_attempts: 5`, and Oban handles retries, deduplication, and rate limiting at the queue layer.

How do I read the API key?

Use `Application.fetch_env!(:my_app, :poststack_api_key)` at runtime. Compile-time reads bake the key into BEAM bytecode and lose the ability to rotate without redeploying.

Related guides

Ready to send emails with Elixir?

Create a free account and get your API key in under a minute.