SDKs

Elixir

Use the official Lettermint Elixir SDK to send email from an Elixir application. The SDK also supports the Team API and webhook signature verification. See SDKs and integrations for other packages.

This guide covers SDK 2.0. If your project uses 1.x, follow the Elixir SDK 2.0 upgrade guide on GitHub.

Requirements

Before you start, you need:

1. Installation

Add the Lettermint Hex package to the existing dependency list in mix.exs:

Elixirmix.exs
defp deps do [ {:lettermint, "~> 2.0"} ] end

Install the dependency:

TerminalCode
mix deps.get

The SDK uses Req for HTTP requests and Jason for JSON. Mix installs these dependencies. No application configuration is required.

2. Send your first email

Set LETTERMINT_PROJECT_TOKEN in your application's environment. Use a Project API token for email sends.

Create a script in your project directory:

Elixirsend_email.exs
lettermint = Lettermint.new(sending_token: System.fetch_env!("LETTERMINT_PROJECT_TOKEN")) {:ok, message} = Lettermint.Emails.send(lettermint, %{ from: "Example <hello@yourdomain.com>", to: ["recipient@example.com"], subject: "Hello from Lettermint", html: "<h1>Hello!</h1><p>This is a test email.</p>", text: "Hello! This is a test email." }) IO.puts("Email ID: #{message.message_id}") IO.puts("Status: #{message.status}")

Replace the sender with an address on your verified domain. Replace the recipient with your test address, then run the script:

TerminalCode
mix run send_email.exs

The client stores nothing about a message, so create it once and share it across processes. You can also pass the token as a string, as in Lettermint.new(token). Options such as timeout: go in the second argument.

API calls return {:ok, result} or {:error, error}, where error is an exception struct. The first example matches a successful response. Use a case expression to handle failures, as shown in the error section below.

Lettermint.Emails.send/3 takes the message in the API's field names, for example reply_to and scheduled_at. The to, cc, and bcc fields are lists.

The Team API uses a separate Team API token. Pass it as team_token:, alone or next to sending_token:. Each part of the client uses its own token and never falls back to the other. See Team API below.

3. Email options

The examples below use the lettermint client from the previous section. Request maps accept atom or string keys.

Recipients, reply-to, and headers

ElixirCode
Lettermint.Emails.send(lettermint, %{ from: "Support <support@yourdomain.com>", to: ["customer@example.com", "colleague@example.com"], cc: ["manager@yourdomain.com"], bcc: ["archive@yourdomain.com"], reply_to: ["help@yourdomain.com"], subject: "Support ticket 12345", text: "Your support ticket has an update.", headers: %{"X-Ticket-ID" => "12345"} })

The payload's headers field sets email headers. Use the idempotency_key request option for the HTTP idempotency header.

Tags and metadata

ElixirCode
Lettermint.Emails.send(lettermint, %{ from: "orders@yourdomain.com", to: ["customer@example.com"], subject: "Order 12345 confirmed", text: "Your order is confirmed.", tags: [%{name: "category", value: "order-confirmation"}], metadata: %{order_id: "12345", customer_id: "cust_789"} })

Use tags for up to 20 name and value pairs, or tag: "order-confirmation" for the existing single tag. The SDK checks tag names and values before it sends the request and returns {:error, %Lettermint.ClientValidationError{}} if one is invalid. Use string values for metadata. Lettermint includes metadata in webhook payloads so you can connect delivery events to application records. See tags for tag options.

Route and delivery settings

ElixirCode
Lettermint.Emails.send(lettermint, %{ from: "notifications@yourdomain.com", to: ["recipient@example.com"], subject: "Your account is ready", text: "You can now sign in.", route: "transactional", settings: %{track_opens: false, track_clicks: true, tls: "enforced"} })

route selects a route by its slug. The tracking settings control open and click tracking for this email. tls: "enforced" requires TLS for email delivery. See tracking and TLS.

Idempotency

Pass an idempotency key as a request option:

ElixirCode
Lettermint.Emails.send( lettermint, %{ from: "orders@yourdomain.com", to: ["customer@example.com"], subject: "Order 12345 confirmed", text: "Your order is confirmed." }, idempotency_key: "order-confirmation-12345" )

Use the same key when you retry the same email. Lettermint.Emails.send_batch/3 and Lettermint.EmailBuilder.send/2 take the same option. The key applies only to that call, and the SDK does not retry requests automatically. See idempotency for the API rules and retention window.

File attachments

Set content to Base64 text or to {:bytes, binary}. The SDK encodes raw bytes to Base64 for you:

ElixirCode
Lettermint.Emails.send(lettermint, %{ from: "invoices@yourdomain.com", to: ["customer@example.com"], subject: "Your invoice", text: "Your invoice is attached.", attachments: [ %{ filename: "invoice.pdf", content: {:bytes, File.read!("/path/to/invoice.pdf")}, content_type: "application/pdf" } ] })

For an inline image, set content_id on the attachment and use the matching cid: reference in the HTML body. See inline images.

Scheduled emails

Use a future timestamp with an explicit time zone:

ElixirCode
scheduled_at = DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.to_iso8601() Lettermint.Emails.send(lettermint, %{ from: "reminders@yourdomain.com", to: ["recipient@example.com"], subject: "Appointment reminder", text: "Your appointment starts in one hour.", scheduled_at: scheduled_at })

The SDK also provides Lettermint.Messages.reschedule/4 and Lettermint.Messages.cancel/3. They accept either token: the team token when you set one, otherwise the sending token. See email scheduling for scheduling limits and cancellation behavior.

Batch sending

Send up to 500 emails in one request with Lettermint.Emails.send_batch/3. The list can mix plain messages and builders (see the next section). The result is a list with one response per email:

ElixirCode
{:ok, results} = Lettermint.Emails.send_batch( lettermint, [ %{from: "hello@yourdomain.com", to: ["user1@example.com"], subject: "Hi", text: "Hello"}, %{from: "hello@yourdomain.com", to: ["user2@example.com"], subject: "Hi", text: "Hello"} ], idempotency_key: "weekly-summary-2026-10-05" ) Enum.each(results, &IO.puts(&1.message_id))

4. Use the pipe operator

Use Lettermint.Emails.compose/2 to build an email in separate steps with Lettermint.EmailBuilder:

ElixirCode
alias Lettermint.EmailBuilder, as: Email lettermint |> Lettermint.Emails.compose() |> Email.from("orders@yourdomain.com") |> Email.to(["customer@example.com"]) |> Email.subject("Order 12345 confirmed") |> Email.html("<h1>Order confirmed</h1><p>Thank you for your order.</p>") |> Email.text("Order confirmed. Thank you for your order.") |> Email.tags([%{name: "category", value: "order-confirmation"}]) |> Email.metadata(%{order_id: "12345"}) |> Email.send(idempotency_key: "order-confirmation-12345")

Each builder function returns a new builder and leaves the original unchanged, so one builder is safe to share and reuse as a template, also across processes. Keep the value that a function returns. The next example uses the Email alias from above:

ElixirCode
welcome = lettermint |> Lettermint.Emails.compose() |> Email.from("Acme <hello@yourdomain.com>") |> Email.subject("Welcome to Acme") welcome |> Email.to("jane@example.com") |> Email.html("<p>Hi Jane</p>") |> Email.send() welcome |> Email.to("john@example.com") |> Email.html("<p>Hi John</p>") |> Email.send()

Use Email.attach/2 to add an attachment and Email.build/1 to get the message in the API's format. Builders also work in Lettermint.Emails.send_batch/3.

5. Responses and errors

Successful JSON responses use generated structs from Lettermint.Types. For an email send, read message_id and status. A field the API did not send is :unset, and enums are open, so give case expressions on values such as status a catch-all clause. API acceptance does not confirm delivery. Use webhooks for delivery and bounce events.

Every error is an exception struct. API errors are Lettermint.APIError and its specific forms, which carry status, code, message, details, and body. Match the structs you want to handle:

ElixirCode
payload = %{ from: "hello@yourdomain.com", to: ["recipient@example.com"], subject: "Hello", text: "This is a test email." } case Lettermint.Emails.send(lettermint, payload, idempotency_key: "hello-recipient-1") do {:ok, message} -> IO.puts("Email ID: #{message.message_id}") {:error, %Lettermint.ValidationError{errors: errors}} -> IO.inspect(errors, label: "Lettermint rejected the message") {:error, %Lettermint.RateLimitError{retry_after: seconds}} -> # Wait seconds, then retry with the same idempotency key. IO.puts(:stderr, "Rate limited. Retry in #{seconds || 1} seconds.") {:error, %Lettermint.TimeoutError{}} -> # The outcome is unknown. Retry with the same idempotency key. IO.puts(:stderr, "The request timed out.") {:error, %Lettermint.APIError{status: status, code: code}} -> IO.puts(:stderr, "Lettermint rejected the request: HTTP #{status} #{code}.") {:error, error} -> IO.puts(:stderr, "The request failed: #{Exception.message(error)}") end

The status-specific structs are Lettermint.AuthenticationError (401), Lettermint.PermissionError (403), Lettermint.NotFoundError (404), Lettermint.ConflictError (409), Lettermint.ValidationError (422), Lettermint.RateLimitError (429), and Lettermint.ServerError (5xx). Other errors are Lettermint.ConnectionError, Lettermint.UnexpectedResponseError, Lettermint.RedirectError, and Lettermint.ClientValidationError, which the SDK returns before it sends the request, for example for an invalid tag. Lettermint.Error provides guards such as is_api_error/1 for matching a whole family.

The SDK never retries a request and never follows a redirect. Reuse the same idempotency key when you retry so the customer gets one email. A missing token, an unrecognised token format, or an invalid option or ID raises Lettermint.ConfigError before any request.

To set the request timeout, pass milliseconds when you create the client:

ElixirCode
lettermint = Lettermint.new( sending_token: System.fetch_env!("LETTERMINT_PROJECT_TOKEN"), timeout: 15_000 )

The timeout covers the whole request, including the response body. Pass timeout: to a single call to override it for that call.

Team API

Use a separate Team API token to manage team resources. Set it in LETTERMINT_TEAM_TOKEN:

ElixirCode
lettermint = Lettermint.new( sending_token: System.get_env("LETTERMINT_PROJECT_TOKEN"), team_token: System.fetch_env!("LETTERMINT_TEAM_TOKEN") ) {:ok, page} = Lettermint.Domains.list(lettermint, %{page: %{size: 25}}) Enum.each(page.data, fn domain -> IO.puts(domain.domain) end)

Query parameters are nested maps. Every list also has an iterate function that returns a lazy Stream and follows next_cursor:

ElixirCode
lettermint |> Lettermint.Domains.iterate(%{filter: %{status: "verified"}}) |> Enum.each(&IO.puts(&1.domain))

Do not pass a Team API token as sending_token:. Each part of the client uses its own token and never falls back to the other. See the Team API quickstart for token setup and permissions.

Track delivery, opens, and bounces

Enable tracking on your route to record opens and clicks. Subscribe to webhooks for delivery and bounce events.

The SDK verifies each delivery for you. Create a verifier from the webhook's signing secret and pass the raw request body and the request headers:

ElixirCode
webhook = Lettermint.Webhook.new(System.fetch_env!("LETTERMINT_WEBHOOK_SECRET")) case Lettermint.Webhook.verify(webhook, raw_body, headers) do {:ok, %Lettermint.WebhookPayload{event: event, data: data}} -> handle(event, data) {:error, %Lettermint.WebhookVerificationError{reason: reason}} -> reject(reason) end

Pass the original request body before JSON decoding. verify/4 requires both the X-Lettermint-Signature and X-Lettermint-Delivery headers and returns the decoded payload. See Webhook signatures for a Phoenix handler and the header and signing secret details.

Use Lettermint with Swoosh

If your application uses Swoosh, use its Lettermint adapter for mail delivery. The official SDK provides direct Sending API and Team API access. The Swoosh adapter remains a separate integration.

Next steps

Tags

Organize emails with tags.

Webhooks

Receive delivery and bounce events.

Test addresses

Test delivery and bounce handling.

Hex package

Find package releases and dependencies.

GitHub repository

Read the source code or report an issue.