Elixir SDK

The official Elixir SDK for Veil Mail with idiomatic tagged tuples, pattern matching, and Phoenix integration.

  • Idiomatic tagged tuple responses (ok/error)
  • Struct-based client (no global Application config)
  • Typed error struct with pattern matching
  • Constant-time HMAC-SHA256 webhook verification
  • Built on Req + Jason (modern Elixir HTTP)
  • Elixir 1.14+, full @spec typespecs

Installation

Add veilmail to your dependencies in mix.exs:

mix.exs
def deps do
  [
    {:veilmail, "~> 0.1.0"}
  ]
end

Quick Start

example.exs
client = VeilMail.client("veil_live_xxxxx")

{:ok, email} = VeilMail.Emails.send(client, %{
  from: "hello@yourdomain.com",
  to: ["user@example.com"],
  subject: "Hello from Elixir!",
  html: "<h1>Welcome!</h1>"
})

IO.inspect(email)

Configuration

Configure the client with keyword options:

config.exs
client = VeilMail.client("veil_live_xxxxx",
  base_url: "https://custom-api.example.com",
  timeout: 10_000
)

Resources

The SDK exposes the same resources as the Node.js SDK:

ModuleDescription
VeilMail.EmailsSend, batch send, list, get, cancel, update emails
VeilMail.DomainsCreate, verify, update, list, delete domains
VeilMail.TemplatesCreate, update, preview, list, delete templates
VeilMail.AudiencesManage audiences and subscribers
VeilMail.CampaignsCreate, schedule, send, pause, resume, cancel campaigns
VeilMail.WebhooksManage webhook endpoints, test, rotate secrets
VeilMail.TopicsManage subscription topics and preferences
VeilMail.PropertiesManage contact property definitions and values

Send Emails

emails.exs
# Simple send
{:ok, email} = VeilMail.Emails.send(client, %{
  from: "hello@yourdomain.com",
  to: ["user@example.com"],
  subject: "Hello!",
  html: "<p>Hello World!</p>",
  tags: ["welcome"]
})

# With template
{:ok, email} = VeilMail.Emails.send(client, %{
  from: "hello@yourdomain.com",
  to: ["user@example.com"],
  templateId: "tmpl_xxx",
  templateData: %{name: "Alice"}
})

# Batch send (up to 100)
{:ok, result} = VeilMail.Emails.send_batch(client, [
  %{from: "hi@yourdomain.com", to: ["user1@example.com"], subject: "Hi", html: "<p>Hi!</p>"},
  %{from: "hi@yourdomain.com", to: ["user2@example.com"], subject: "Hi", html: "<p>Hi!</p>"}
])

Subscriber Management

subscribers.exs
# Add a subscriber
{:ok, subscriber} = VeilMail.Audiences.add_subscriber(client, "audience_xxxxx", %{
  email: "user@example.com",
  firstName: "Alice",
  lastName: "Smith"
})

# List subscribers
{:ok, result} = VeilMail.Audiences.list_subscribers(client, "audience_xxxxx",
  limit: "50",
  status: "active"
)

# Export as CSV
{:ok, csv} = VeilMail.Audiences.export_subscribers(client, "audience_xxxxx")

Error Handling

Use pattern matching on the VeilMail.Error struct:

errors.exs
case VeilMail.Emails.send(client, params) do
  {:ok, email} ->
    IO.puts("Sent: #{email["id"]}")

  {:error, %VeilMail.Error{type: :authentication, message: msg}} ->
    IO.puts("Invalid API key: #{msg}")

  {:error, %VeilMail.Error{type: :pii_detected, pii_types: types}} ->
    IO.puts("PII detected: #{inspect(types)}")

  {:error, %VeilMail.Error{type: :rate_limit, retry_after: retry}} ->
    IO.puts("Rate limited, retry after #{retry} seconds")

  {:error, %VeilMail.Error{type: :validation, message: msg}} ->
    IO.puts("Validation: #{msg}")

  {:error, %VeilMail.Error{message: msg}} ->
    IO.puts("Error: #{msg}")
end

Webhook Verification

The SDK provides VeilMail.Webhook for constant-time HMAC-SHA256 verification:

webhook_controller.ex
defmodule MyAppWeb.WebhookController do
  use MyAppWeb, :controller

  @webhook_secret "whsec_xxxxx"

  def handle(conn, _params) do
    {:ok, body, conn} = Plug.Conn.read_body(conn)
    signature = Plug.Conn.get_req_header(conn, "x-signature-hash") |> List.first("")

    case VeilMail.Webhook.verify(body, signature, @webhook_secret) do
      {:ok, payload} ->
        process_event(payload)
        send_resp(conn, 200, "OK")

      {:error, _reason} ->
        send_resp(conn, 401, "Invalid signature")
    end
  end

  defp process_event(%{"type" => "email.delivered", "data" => data}) do
    IO.inspect(data, label: "Delivered")
  end

  defp process_event(%{"type" => "email.bounced", "data" => data}) do
    IO.inspect(data, label: "Bounced")
  end

  defp process_event(_event), do: :ok
end

No Global State

The client is a struct, not Application-level config. This means you can have multiple clients with different API keys, tests run concurrently without conflicts, and configuration is explicit at every call site. Store the client in your application's supervision tree or pass it through your context.

Required Scopes

The Elixir SDK uses the same API scopes as the Authentication system. See the Node.js SDK documentation for the full scope reference.