CheckDisposable Emailcheckdisposable.email
← All guidesPhoenix (Elixir) guide · Elixir

Block disposable email signups in Phoenix

Add a custom Ecto changeset validation. The check runs as part of `cast → validate` chain so disposable emails surface as standard `changeset.errors`.

The code

# lib/myapp/disposable_email.ex
defmodule MyApp.DisposableEmail do
  @url "https://api.checkdisposable.email/v1/check"

  def disposable?(email) when is_binary(email) do
    key = System.fetch_env!("CDE_KEY")
    url = "#{@url}?email=#{URI.encode_www_form(email)}"
    case Req.get(url, headers: [{"authorization", "Bearer #{key}"}], receive_timeout: 3_000) do
      {:ok, %{status: 200, body: %{"is_disposable" => true}}} -> true
      _ -> false # fail open on any error
    end
  end
end

# lib/myapp/accounts/user.ex
defmodule MyApp.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias MyApp.DisposableEmail

  schema "users" do
    field :email, :string
    field :hashed_password, :string, redact: true
    timestamps()
  end

  def registration_changeset(user, attrs) do
    user
    |> cast(attrs, [:email])
    |> validate_required([:email])
    |> validate_format(:email, ~r/@/)
    |> validate_not_disposable()
    |> unique_constraint(:email)
  end

  defp validate_not_disposable(changeset) do
    case get_change(changeset, :email) do
      nil -> changeset
      email ->
        if DisposableEmail.disposable?(email),
          do: add_error(changeset, :email, "is not allowed — please use a real address"),
          else: changeset
    end
  end
end

Notes

Req over HTTPoison
Req is the modern HTTP client in the Elixir ecosystem — built on Finch, with sane defaults. Add `{:req, "~> 0.5"}` to mix.exs.
phx.gen.auth
If you generated auth with `mix phx.gen.auth`, drop `validate_not_disposable()` into the existing `registration_changeset/2`. No other changes needed.
LiveView UX
In LiveView signup forms, the changeset error renders inline via `<.error :for={msg <- @form[:email].errors} />` without any JS — Phoenix replaces the field in place via the websocket.

Get a free API key

500 checks/month, no credit card. No credit card. 30 seconds.

Sign up free →