Packages

Official Elixir client for the send.io email API, with a Swoosh adapter.

Current section

Files

Jump to
sendio README.md
Raw

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