Packages
Official Elixir client for the send.io email API, with a Swoosh adapter.
Current section
Files
Jump to
Current section
Files
README.md
# send.io Elixir SDK
The official Elixir client for the [send.io](https://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`:
```elixir
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:
```elixir
# 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.
```elixir
{: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:
```elixir
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:
```elixir
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
```elixir
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`.
```elixir
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).
```elixir
# 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
```elixir
{:ok, email} = Sendio.Emails.get("4ef9a417-02e9-4d39-ad75-9611e0fcc33c")
email.last_event
{:ok, list} = Sendio.Emails.list()
```
## Domains
```elixir
{: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.
```elixir
{: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.
```elixir
{: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
```elixir
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](https://hexdocs.pm/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:
```elixir
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{}`:
```elixir
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
```elixir
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:
```sh
mix test
```
Integration tests create and delete real resources in your send.io account:
```sh
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](LICENSE) and [NOTICE](NOTICE).