Skip to content
Browse the docs

Frameworks and agents · Published 2026-10-31 · Updated 2026-10-31 · AgentiSend

Send email with Phoenix

A Phoenix controller action that posts one booking notice through :httpc, which ships with Erlang.

To send email with Phoenix, call one function from a controller action that posts JSON to POST /emails. The controllers guide says an action is a function of conn and params, that the router names it as post "/send", ReceiptController, :create, and that json/2 renders a map. There is no Elixir package for this API, and this page does not invent one. :httpc ships with Erlang. Send email with Elixir is that client with no controller. The function below checks the address, sends a booking with an idempotency key derived from the guest, and returns the message id or the refusal's code and fix. The suite runs elixir send.exs against a local AgentiSend server. It does not boot Phoenix.

Install

No package to add. The function uses :httpc. The action renders with json/2:

def create(conn, %{"email" => email, "note" => note}) do
  {status, body} = ReceiptMail.send(email, note)
  conn |> put_status(status) |> json(body)
end

Environment

VariableRequiredWhat it is
AGENTISEND_API_KEYyesA key with sending_access.
MAIL_FROMyesThe From address, on a domain you have verified.
AGENTISEND_BASE_URLnoDefaults to https://api.agentisend.com.

The route

# Phoenix — the function a controller action calls.
#
# There is no Elixir package for this API. :httpc ships with Erlang. A
# Phoenix action reads params and passes email and note here, then renders
# the map with json/2. This file does not boot Phoenix.
#
# AGENTISEND_API_KEY is the key. MAIL_FROM is an address on a domain you have
# verified. AGENTISEND_BASE_URL is optional and defaults to the public API.
#
# Run: elixir send.exs

:inets.start()
:ssl.start()

defmodule ReceiptMail do
  @address ~r/^[^@\s]+@[^@\s]+\.[^@\s]+$/

  def send(email, note) do
    email = String.trim(email)

    if Regex.match?(@address, email) do
      post(email, note)
    else
      {400, %{"error" => "email must be one address, like you@example.com"}}
    end
  end

  defp env!(name) do
    case System.get_env(name) do
      nil -> raise "Set #{name}."
      "" -> raise "Set #{name}."
      value -> value
    end
  end

  defp base_url do
    case System.get_env("AGENTISEND_BASE_URL") do
      nil -> "https://api.agentisend.com"
      "" -> "https://api.agentisend.com"
      value -> String.trim_trailing(value, "/")
    end
  end

  defp json_string(raw, key) do
    case Regex.run(~r/"#{key}"\s*:\s*"([^"]*)"/, raw) do
      [_, value] -> value
      _ -> nil
    end
  end

  defp post(email, note) do
    from = env!("MAIL_FROM")
    payload =
      ~s({"from":"#{from}","to":"#{email}","subject":"Your table is booked","text":"Booking note: #{note}"})

    headers = [
      {'authorization', String.to_charlist("Bearer " <> env!("AGENTISEND_API_KEY"))},
      {'accept', 'application/json'},
      # The guest, not the moment: a retry of the same note replays.
      {'idempotency-key', String.to_charlist("table/" <> email)}
    ]

    request =
      {String.to_charlist(base_url() <> "/emails"), headers, 'application/json',
       String.to_charlist(payload)}

    case :httpc.request(:post, request, [{:timeout, 30_000}], []) do
      {:ok, {{_, status, _}, _, body}} ->
        text = to_string(body)

        if status >= 400 do
          {status, %{"code" => json_string(text, "code"), "fix" => json_string(text, "fix")}}
        else
          {status, %{"id" => json_string(text, "id")}}
        end

      other ->
        raise "request failed: #{inspect(other)}"
    end
  end
end

defmodule Emit do
  def line(step, {status, fields}) do
    pairs =
      [~s("step":"#{step}"), ~s("status":#{status})] ++
        Enum.flat_map(fields, fn
          {_key, nil} -> []
          {key, value} -> [~s("#{key}":"#{value}")]
        end)

    IO.puts("{" <> Enum.join(pairs, ",") <> "}")
  end
end

Emit.line("send", ReceiptMail.send("phoenix@example.com", "first"))
Emit.line("retry", ReceiptMail.send("phoenix@example.com", "first"))
Emit.line("invalid", ReceiptMail.send("not-an-address", "first"))
Emit.line("refused", ReceiptMail.send("phoenix@example.com", "second"))
examples/phoenix/send.exs, run by the test suite against a local AgentiSend server

Try it

elixir send.exs

The script prints one JSON object per step: the booking, the same request again, a malformed address answered before any request, and a changed note under the same key.

What comes back when it goes wrong

Every refused request carries code, message, fix and docs_url. The function returns code and fix with the API's status, and put_status passes that status through. The error catalogue lists every code.

Next