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"}
  ]
end

Quick 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.id

Or 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}")
end

Errors 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 it

Automation 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
OptionDefaultNotes
:api_keySENDIO_API_KEY env varrequired
:base_urlSENDIO_BASE_URL env var, then https://api.send.iotrailing slash optional
:clientSendio.Client.ReqClientimplement Sendio.Client to swap the HTTP client
:req_optionsnoneextra 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
end

Swoosh 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.

License

Apache-2.0. See LICENSE and NOTICE.