send.io Elixir SDK
The official Elixir client for the send.io email API, with a Swoosh adapter. Full API reference: https://developer.send.io/docs/api.
Installation
Add sendio to your dependencies in mix.exs:
def deps do
[
{:sendio, "~> 1.0"}
]
endQuick start
Create an API key in the send.io dashboard (keys start with mx_) and configure it:
# config/runtime.exs
config :sendio, Sendio.Client, api_key: System.fetch_env!("SENDIO_API_KEY")If nothing is configured, the SENDIO_API_KEY environment variable is used.
{:ok, email} =
Sendio.Emails.send(%{
from: "Acme <onboarding@yourdomain.com>",
to: "delivered@send.io",
subject: "Hello from send.io",
html: "<strong>It works!</strong>"
})
email.idOr build a client explicitly and pass it as the first argument:
client = Sendio.client(api_key: "mx_123456789")
Sendio.Emails.send(client, %{
from: "Acme <onboarding@yourdomain.com>",
to: "delivered@send.io",
subject: "Hello!",
text: "It works!"
})Elixir script:
Mix.install([{:sendio, "~> 1.0"}])
client = Sendio.client(api_key: System.fetch_env!("SENDIO_API_KEY"))
Sendio.Emails.send(client, %{
from: "Acme <onboarding@yourdomain.com>",
to: "delivered@send.io",
subject: "Hello!",
text: "It works!"
})Send a batch
Sendio.Emails.send_batch([
%{from: "Acme <onboarding@yourdomain.com>", to: "delivered@send.io", subject: "First", html: "<p>one</p>"},
%{from: "Acme <onboarding@yourdomain.com>", to: "delivered@send.io", subject: "Second", html: "<p>two</p>"}
])Idempotency keys
Pass idempotency_key: after the parameters to make a retry safe: the same key returns the first result instead of sending again. It is sent as the Idempotency-Key header and works for send and send_batch.
Sendio.Emails.send(
%{from: "Acme <onboarding@yourdomain.com>", to: "delivered@send.io", subject: "Welcome", html: "<p>Welcome aboard!</p>"},
idempotency_key: "welcome-user/123"
)
Sendio.Emails.send_batch(emails, idempotency_key: "digest/2026-10-08")
# with an explicit client
Sendio.Emails.send(client, params, idempotency_key: "welcome-user/123")Verify webhooks
Sendio.Webhooks.verify/3 checks the svix-id, svix-timestamp and svix-signature headers against the raw request body and your webhook's signing secret (whsec_...), then returns the decoded event. The timestamp must be within 5 minutes (verify/4 takes tolerance: seconds), and the header may carry several v1 signatures (for example during a secret rotation).
# In a Phoenix/Plug endpoint; read the raw body before any JSON parser does
{:ok, payload, conn} = Plug.Conn.read_body(conn)
secret = System.fetch_env!("SENDIO_WEBHOOK_SECRET")
case Sendio.Webhooks.verify(payload, conn.req_headers, secret) do
{:ok, event} -> IO.inspect(event["type"])
{:error, reason} -> Plug.Conn.send_resp(conn, 400, "invalid signature: #{reason}")
endErrors are :missing_header, :invalid_timestamp, :timestamp_out_of_tolerance, :invalid_secret, :invalid_signature and :invalid_payload.
Retrieve an email
{:ok, email} = Sendio.Emails.get("4ef9a417-02e9-4d39-ad75-9611e0fcc33c")
email.last_event
{:ok, list} = Sendio.Emails.list()Domains
{:ok, domain} = Sendio.Domains.create(%{name: "yourdomain.com"})
Sendio.Domains.verify(domain.id)
Sendio.Domains.get(domain.id)
Sendio.Domains.list()
Sendio.Domains.remove(domain.id)
# Receive email on the domain too (send.io then requires its MX record)
{:ok, domain} = Sendio.Domains.create(name: "yourdomain.com", capabilities: %{receiving: "enabled"})
# Stop receiving: send.io stops accepting email for the domain; remove its MX record
Sendio.Domains.update(domain.id, capabilities: %{receiving: "disabled"})
# Turn it back on (asks for the domain's MX record; publish it to start receiving email)
Sendio.Domains.update(domain.id, capabilities: %{receiving: "enabled"})Sending is always on; capabilities: %{sending: "disabled"} is rejected with a 422.
Mailboxes
Mailboxes are real email accounts with an IMAP/SMTP login. The domain must be verified.
{:ok, mailbox} = Sendio.Mailboxes.create(address: "support@yourdomain.com")
# Shown once; the password is also an API key for this mailbox.
mailbox.credentials
Sendio.Mailboxes.list()
Sendio.Mailboxes.update(mailbox.id, warmup_enabled: false)
Sendio.Mailboxes.reset_password(mailbox.id) # the old password stops working
Sendio.Mailboxes.remove(mailbox.id) # also removes an agent inbox on itAutomation templates
Prebuilt workflows with their emails. Creating one sets up the events, emails and a disabled automation.
{:ok, catalog} = Sendio.Automations.Templates.list(category: "onboarding")
{:ok, template} = Sendio.Automations.Templates.get("email-verification-reminder")
{:ok, automation} =
Sendio.Automations.create_from_template(
%{
slug: template.slug,
version: template.version,
sender: %{from: "Acme <hello@yourdomain.com>"},
params: %{resend_url: "https://app.yourdomain.com/verify-email"}
},
idempotency_key: "setup/verify-reminders"
)API modules
Sendio.Emails, Sendio.Domains, Sendio.ApiKeys, Sendio.Audiences, Sendio.Automations, Sendio.Automations.Templates, Sendio.Broadcasts, Sendio.Contacts, Sendio.ContactProperties, Sendio.Mailboxes, Sendio.ReceivedEmails, Sendio.Segments, Sendio.Templates, Sendio.Topics, Sendio.Webhooks. Every function takes an optional client as its first argument.
Configuration
config :sendio, Sendio.Client,
api_key: "mx_123456789",
base_url: "https://api.send.io",
client: Sendio.Client.ReqClient| Option | Default | Notes |
|---|---|---|
:api_key | SENDIO_API_KEY env var | required |
:base_url | SENDIO_BASE_URL env var, then https://api.send.io | trailing slash optional |
:client | Sendio.Client.ReqClient | implement Sendio.Client to swap the HTTP client |
:req_options | none | extra Req options (extra headers are added, not replaced) |
Point the SDK at another environment (for example a local test server) with the environment variable or per client:
Sendio.client(api_key: "mx_123", base_url: "http://localhost:8080")Every request carries User-Agent: sendio-elixir:<version>.
Error handling
Functions return {:ok, result} or {:error, reason}. API errors are returned as %Sendio.Error{}:
case Sendio.Emails.send(%{from: "bad", to: "delivered@send.io", subject: "x", text: "x"}) do
{:ok, email} -> email.id
{:error, %Sendio.Error{name: name, message: message, status_code: status}} -> {status, name, message}
{:error, :client_error} -> :network_error
endSwoosh adapter
config :my_app, MyApp.Mailer,
adapter: Sendio.Swoosh.Adapter,
api_key: "mx_123456789"In production, configure the adapter in prod.exs and the key in runtime.exs. Every mail your Phoenix app sends, including phx.gen.auth emails, then goes through send.io.
Testing
Unit tests use mocks and need no configuration:
mix test
Integration tests create and delete real resources in your send.io account:
SENDIO_API_KEY="mx_123456789" SENDIO_TEST_FROM="you@yourdomain.com" mix test --include integration
examples/smoke.exs is a short live check (sends one email): SENDIO_API_KEY=mx_... mix run examples/smoke.exs.