Elixir developers choose the BEAM for resilient processes, explicit supervision, and systems that remain understandable as traffic grows. But an Elixir email API integration can still become an awkward edge of the stack: local mailbox previews differ from production delivery, release-time configuration can accidentally capture the wrong secret, and SMTP introduces connections, TLS settings, and timeout behavior that do not belong in your core product logic.
Volanea gives Elixir applications an HTTP-first path for sending transactional email. Keep your application focused on composing the right event—receipt, password reset, invitation, alert, or verification—and send it through a REST API built for production email operations.
Email sending should fit the Elixir way of building systems
A Phoenix application is rarely just a collection of request handlers. It may include Oban workers, GenServers, scheduled jobs, LiveView actions, umbrella applications, and background processes running under one supervision tree. Email belongs naturally in that environment, but the actual handoff to an email provider needs to be predictable.
That means a good integration should preserve the things Elixir teams already value:
- Clear boundaries: application code decides when and why to send; email infrastructure handles the delivery pipeline.
- Runtime configuration: production credentials are injected when a release boots, not compiled into a build artifact.
- Explicit failure handling: an HTTP response is a concrete result your process can classify, log, retry, or route to a dead-letter workflow.
- Portable deployment: the same integration works from a long-running Phoenix release, a worker process, a container job, or an HTTP-capable serverless environment.
- Testable application code: your tests can exercise message construction and your sending boundary separately from live delivery.
The result is less time spent turning a mail transport into a source of operational risk. Your application sends a structured request; Volanea processes the email delivery workflow behind it.
Why Elixir teams hit email friction in production
Elixir makes concurrency approachable, but it does not erase the differences between local development and a real deployment. Email tends to expose those differences early because it crosses several boundaries at once: your runtime, your secrets manager, DNS, recipient mail systems, and the provider that performs final delivery.
Local development is intentionally not production email
In development, it is sensible to preview messages locally rather than deliver real mail. Libraries such as Swoosh support composing and testing emails, and Phoenix projects often use mailbox preview workflows during development. That is excellent for validating copy, templates, links, and rendering.
But a local preview cannot tell you whether a production domain is authenticated, whether an address is suppressed after a hard bounce, whether a recipient server accepted the message, or whether a provider event needs to update your account state. The useful distinction is simple: local preview validates the message your app created; production delivery validates the system that sends it.
Treat those as two separate concerns. Keep a fast feedback loop for email composition, then use a production-grade sending path for the actual handoff.
Releases make secret timing matter
Elixir releases package application code, dependencies, and the Erlang runtime into a deployable artifact. That portability is valuable, but it also creates an important configuration rule: environment-specific values such as API keys should be read at runtime rather than baked into an artifact during compilation.
If a credential is read too early, a staging key can become embedded in a production build, a rotation can require a rebuild instead of a restart, or a build system can fail because it does not have access to a deployment secret. A safer model is to inject VOLANEA_API_KEY through your platform’s secret store and read it only where your production configuration is assembled.
This is not a theoretical distinction. It affects how confidently you can promote the same release through development, staging, and production. Build once, configure at runtime, and keep credentials out of source control, logs, exception messages, and client-side code.
SMTP adds a network protocol to your application boundary
SMTP remains useful, particularly when you are working with an existing adapter or a legacy integration. In Elixir, Swoosh includes an SMTP adapter, which can be a reasonable option for systems that already have a stable SMTP configuration.
However, SMTP also brings relay hostnames, ports, authentication, TLS negotiation, connection behavior, and socket timeouts into your deployment surface. Under bursty traffic, every one of those details deserves operational attention. A REST API shifts that boundary to ordinary outbound HTTPS, which fits naturally with the HTTP clients, observability, reverse proxies, and egress controls many teams already operate.
The point is not that SMTP is inherently wrong. It is that transactional email is usually easier to reason about when the application makes a bounded HTTP request and receives a structured response.
Send email from Elixir with one HTTP boundary
Volanea’s send endpoint is POST /v1/send. For an Elixir application, the cleanest pattern is a small module that owns the provider request. Call that module from a controller, a context, an Oban worker, or a supervised process—but do not scatter provider details across the codebase.
Req is a practical choice because it is an Elixir HTTP client with direct support for JSON request bodies. The short example below shows the shape of that boundary. Configure the base URL and authentication header according to the email API reference and setup guides, and keep the key in your runtime environment.
defmodule MyApp.Email do
def send_welcome(email) do
Req.post(
url: System.fetch_env!("VOLANEA_API_URL") <> "/v1/send",
headers: [{"authorization", System.fetch_env!("VOLANEA_API_KEY")}],
json: %{
from: "hello@example.com",
to: email,
subject: "Welcome to MyApp",
html: "<p>Your account is ready.</p>"
}
)
end
end
Keep this module deliberately boring. It should accept application-level data, construct a valid delivery request, call Volanea, and return the result. It should not contain business decisions that belong elsewhere, such as whether a new user qualifies for an onboarding campaign or whether an invoice should be resent.
Design the module around outcomes, not provider calls
The code that invokes your mailer generally needs one of three outcomes:
- Accepted: the provider accepted the request for processing.
- Rejected without retry: the request is invalid, the sender is not configured, or the recipient data should be corrected before another attempt.
- Temporarily unsuccessful: a network or service issue means the job may be retried safely under your retry policy.
That contract is more important than the particular HTTP client you use. It gives a background worker enough information to choose a retry, it gives a controller enough information to return a safe response, and it gives your logs a useful event to correlate with a customer action.
Avoid turning every response into an exception. A malformed recipient or an unverified sender should be visible and actionable, but it is not necessarily a system crash. Return explicit tagged results from your wrapper module, then let the caller decide how that failure affects the user flow.
Use queues for work that must survive request lifecycles
A password reset email is usually time-sensitive, but the web request that creates it does not need to remain open while your application waits on downstream delivery work. A receipt may need stronger guarantees than an in-request best effort. A notification burst may need controlled concurrency rather than hundreds of simultaneous sends.
For those cases, enqueue the intent to send after the underlying business transaction succeeds. An Oban worker or comparable job processor can call your Volanea module, apply bounded retries to transient failures, and record enough context for later support work.
Be careful with retries. Email is not a pure idempotent side effect from the recipient’s perspective: retrying an ambiguous request can produce two messages. Store an application-level notification record or delivery intent before sending. Associate it with a stable business event—such as invoice:1234:receipt:v1—and prevent duplicate work at the job layer where appropriate.
REST is the practical default for modern deployments
The best email transport depends on where your Elixir code actually runs. A traditional Phoenix release on a long-lived VM has different constraints from a short-lived container task or an edge-adjacent service.
Long-lived Phoenix and OTP deployments
For a conventional deployment, both SMTP and HTTPS can work. The operational question is which interface your team can support most reliably. HTTPS requests are familiar to most application teams: they pass through standard egress paths, can use existing instrumentation, and expose status codes and JSON responses.
A REST API also avoids forcing every application process to own knowledge of SMTP transport behavior. Your code can use the same request discipline it uses for payments, storage, analytics, or internal services: explicit timeouts, structured logs, retries only where justified, and a narrow adapter module.
Containers and short-lived jobs
Containers scale well for worker pools and background tasks, but task lifetimes and connection reuse can vary by platform. A fresh container or short-lived process may not benefit from the persistent socket behavior an SMTP-focused integration expects. An outbound HTTPS request remains a straightforward primitive: establish a secure connection, make the request, classify the response, and terminate cleanly if the workload is complete.
This does not mean you should retry every timeout immediately. A short timeout can protect worker capacity, but overly aggressive retries can amplify a temporary incident. Choose a timeout that matches your provider policy and deployment environment, then use exponential backoff with a maximum attempt count for failures you have classified as transient.
Edge and restricted runtimes
An Elixir application itself is usually deployed to a BEAM-capable environment rather than a JavaScript edge isolate. Still, modern architectures often place email-triggering logic in companion edge routes, authentication hooks, or serverless functions written in another language. In those environments, raw SMTP may be unavailable or restricted.
For example, Cloudflare Workers prohibit outbound TCP connections to port 25, the common SMTP server port. An HTTPS-based email API is therefore the portable option for edge-triggered workflows. The same Volanea REST interface lets a Phoenix backend, a queue consumer, and a lightweight edge function use one delivery model instead of maintaining separate SMTP behavior for each runtime.
That architectural consistency matters when your authentication event occurs at the edge but account records and lifecycle logic live in Phoenix. Use a secure server-to-server path, avoid exposing the API key in browser code, and make the email request from a trusted runtime.
Deliverability starts before your Elixir code sends
A successful HTTP response is only one stage of responsible email delivery. Deliverability is shaped by sender identity, recipient quality, message relevance, complaint handling, authentication, and the consistency of your sending behavior over time.
Your Elixir application should generate correct and timely messages. Your email platform should give those messages an authenticated sending identity and a delivery workflow that can account for bounces, complaints, suppressions, tracking, and events.
Authenticate the domain you send from
Use a domain you control for the from address. Domain authentication records establish that your sending platform is authorized to send on behalf of that domain, and they help recipient systems evaluate whether the message is aligned with the visible sender identity.
The operational takeaway for developers is direct: do not make production sender addresses a casual string scattered through templates. Treat the from domain as a configuration decision with ownership, review, and DNS setup behind it. Use a verified domain in production and make staging send from a separate, intentionally configured identity.
Volanea’s sending flow supports verified domains, and its batch sending documentation notes that senders must belong to a verified domain outside test mode. That is a useful guardrail: it pushes teams toward a sender identity that is ready for real recipients rather than accidentally shipping from a placeholder address.
Separate transactional and promotional intent
A password reset is requested by a user. A product announcement is a marketing decision. A security alert may need to reach every affected account quickly. Those categories carry different expectations, consent requirements, cadence rules, and consequences when a recipient complains.
Keep them distinct in your application model. Transactional messages should be triggered by a clear product event and contain the information needed to complete it. Marketing messages should use consent-aware audience logic, campaign-level content controls, and an easy path to honor preferences.
Volanea combines transactional sending and campaign capabilities, which can reduce the need to maintain separate contact states across disconnected systems. But a unified platform does not remove the need for disciplined intent. Your data model should still answer: why is this person receiving this email, what preference applies, and what action should a bounce or complaint trigger?
Validate addresses before costly or sensitive flows
Email typos are common, especially in invitation and signup workflows. A bad address increases support volume, wastes a message attempt, and can contaminate audience data if it is stored without review.
Validate basic formatting in your application, but do not mistake a format check for deliverability verification. If a workflow depends on address quality—bulk invitations, account recovery, high-value onboarding, or campaign imports—use an address verification step before sending at scale. Volanea provides a free email address verification tool for checking addresses before they enter a sending workflow.
For security-sensitive emails, avoid revealing whether an address exists through different UI messages. Your application can accept a password-reset request uniformly while logging the internal decision and sending only when an account is eligible.
Build delivery behavior that respects the BEAM
The BEAM makes it easy to run many lightweight processes. That is a powerful reason to use Elixir, but it should not lead to unlimited email concurrency just because spawning tasks is cheap.
A delivery provider, recipient domain, or your own deployment can still become the limiting resource. Good email architecture uses concurrency intentionally.
Bound concurrency at the worker layer
If a scheduled task needs to send thousands of notifications, do not turn a database query directly into thousands of uncoordinated tasks. Use a queue with a configured concurrency limit, batch where it fits the use case, and retain enough metadata to reconcile outcomes.
Volanea’s batch endpoint accepts up to 1,000 personalized messages in one call, while the single-message send endpoint can address one recipient or up to 50 recipients. Those options are useful, but they do not mean every workload should use the largest payload. Choose the unit of work that matches your failure semantics.
A single invoice receipt is naturally a single-message event. A notification to a bounded group may fit one request with multiple recipients if every recipient is intended to receive identical content and privacy requirements allow it. A lifecycle operation with personalized content may be better as a batch, provided your application can interpret each individual result.
Keep recipient privacy in the data model
Email APIs can technically support multiple recipients, but product requirements should decide whether that is appropriate. If recipients should not see one another, do not place unrelated customer addresses into visible recipient fields. For most application notifications, one recipient per personalized message is the clearest and safest model.
This also simplifies support. When a customer says they did not receive a message, you can inspect one delivery intent, one message payload version, one recipient, and the associated provider event trail. That is much easier than disentangling a large send created for convenience.
Make timeouts and retries part of the contract
A timeout is not proof that an email was not accepted. It is proof that your process did not receive a conclusive response in time. That distinction should shape retry behavior.
For each message category, decide what is acceptable:
- Password resets: retries should be prompt but bounded; reset tokens must remain valid for the intended window.
- Receipts and invoices: preserve an auditable record of the event and retry transient delivery handoffs carefully.
- Operational alerts: prioritize speed, but avoid retry storms during an outage.
- Marketing sends: use campaign controls and measured throughput rather than tying delivery to live web requests.
Use structured logging with a notification ID, your business event ID, recipient identifier or safely hashed address, message category, attempt number, and provider response classification. Do not log access keys or full sensitive content.
Keep secrets and sender configuration out of application code
Email credentials can send mail as your organization. Treat them as production secrets, not configuration conveniences.
Use runtime configuration deliberately
In a Phoenix release, place environment-sensitive setup in config/runtime.exs or in the platform mechanism that prepares runtime configuration. Your email boundary can read a configured value rather than fetching process environment variables throughout application code.
Centralizing configuration has practical benefits. It makes missing secrets fail early and clearly during boot, it reduces the chance of accidentally using a development credential in production, and it makes key rotation a configuration operation rather than a code change.
Separate keys by environment. Development, staging, and production should not share one all-powerful credential. If a staging log is accidentally retained too long or a local .env file leaks, the blast radius is smaller when that key cannot send from production domains.
Never send through browser code
A Volanea API key belongs only in a trusted server runtime. A LiveView event, controller action, or JSON API endpoint can receive a user action, authorize it, and call your email module. The browser should never receive the credential or construct a provider request directly.
This applies equally to companion JavaScript services and edge code. Put the secret in the platform’s server-side secret store, restrict access to the runtime that sends email, and rotate the key if it ever appears in source control, logs, monitoring payloads, or a client bundle.
Treat the sender as a product asset
The from address is not merely technical configuration. Recipients use it to decide whether a message is trustworthy. Support teams rely on it when diagnosing issues. Domain authentication depends on it. Campaign preferences and reply handling may depend on it.
Choose a stable sender identity for each message stream. For example, security mail, billing mail, and marketing mail can use clearly owned addresses while sharing a verified organizational domain. Make ownership explicit so DNS changes, reply handling, and content changes do not become orphaned tasks.
Testing email without making tests flaky
A strong Elixir email implementation separates message creation from live provider delivery. That lets you test most of the system quickly and deterministically.
Test the message intent
Unit-test that a password-reset workflow produces the correct subject, recipient, tokenized link, and template data. Test that an invoice receipt references the correct order and does not send before payment is confirmed. Test authorization: a user should not be able to trigger mail to another user’s address through an insecure endpoint.
These are application tests. They should not require a network request.
Test the provider boundary
At the HTTP boundary, use a test adapter or request mock to assert that your module constructs the expected method, endpoint path, headers, and JSON shape. Test success, validation failure, temporary failure, and malformed provider responses. Confirm that keys are not included in logs when an error occurs.
Then maintain a small staging smoke test that sends to controlled inboxes from an authenticated staging domain. This is where you verify the pieces unit tests cannot: runtime secrets, DNS, verified sender configuration, provider acceptance, and actual mailbox behavior.
Test the workflow after the send
For many product flows, acceptance is not the final state. You may need to process bounce, complaint, delivered, opened, or clicked events according to the features you use. Keep your webhook or event consumer idempotent: the same event may be delivered more than once, events may arrive out of order, and a database update should be safe to repeat.
Do not use opens as a definitive measure of human attention, and do not let a missing open event break a business workflow. Operational delivery data is most valuable when it drives concrete actions: suppress a hard-bounced address, investigate elevated complaints, retry a known temporary failure where appropriate, or show support a clear timeline.
One platform for product email and lifecycle growth
Early in a product’s life, email often starts as a password-reset requirement. Then it becomes receipts, team invitations, account verification, trial reminders, security notices, onboarding sequences, product announcements, and campaigns. The technical integration can remain small while the operational surface expands.
Volanea is designed for both transactional email and campaigns, so teams can use a shared email infrastructure as their needs grow. That matters because contact data, suppression state, sender domains, and delivery events are more useful when they do not have to be reconciled across multiple disconnected tools.
For developers, the benefit is not adding marketing logic to Phoenix controllers. It is establishing one reliable sending foundation. Your product team can define campaigns and lifecycle programs while your application retains a clear, server-side API boundary for event-driven transactional mail.
As volume changes, review your sending plan alongside the workloads you actually operate: expected transactional volume, campaign volume, domains, environments, and support needs. See transactional email pricing when you need to map sending volume to the right plan rather than guessing from a generic monthly estimate.
A practical rollout plan for Elixir teams
A disciplined rollout is usually faster than trying to migrate every message type at once. Start with one low-risk but meaningful transactional flow, establish the operational pattern, and expand from there.
- Choose a verified production sender domain. Decide who owns DNS, the visible from address, and reply handling.
- Create a runtime-only configuration path. Keep the Volanea key in your deployment secret manager and load it when the release starts.
- Build one small email module. Put provider requests in a narrow boundary rather than calling the API throughout controllers and contexts.
- Send one transactional message type first. Account verification or a test-only internal notification can validate the full path without risking critical billing or security workflows.
- Add structured logs and notification records. Make every request traceable to a business event without storing unnecessary sensitive data.
- Move important sends to background jobs. Use bounded concurrency and explicit retry policies for messages that need durability.
- Process delivery feedback. Update your suppression and support workflows based on bounces, complaints, and other relevant events.
- Expand intentionally. Add receipts, invitations, alerts, and lifecycle messages only after the core path is observable and reliable.
This progression gives your team a chance to answer real operational questions early: Which failures are safe to retry? Who investigates a sender-domain problem? Where does a support agent look when a customer says they did not receive an email? How do you prevent an event replay from sending two receipts?
Those answers are what make email infrastructure dependable—not simply the fact that a request returned successfully once.
Build email delivery into the system, not around it
Elixir is at its best when boundaries are explicit and failure is handled as part of normal system design. Email should follow the same pattern.
Use a REST-based Volanea integration to keep delivery as a clear outbound dependency. Configure secrets at runtime. Authenticate your sender domain. Queue work that must survive request lifecycles. Bound concurrency. Classify failures. Store delivery intent. Process the feedback that matters.
That approach leaves Phoenix, OTP, and your business domains free to do what they do best: model the product correctly and keep it running reliably. Volanea handles the email infrastructure layer so your team can send transactional email with a workflow that fits the way Elixir applications are actually deployed.
FAQ
What is the best way to send email from an Elixir application?
For a production application, keep email delivery behind a small module and call an HTTPS email API from that boundary. This makes configuration, timeouts, retries, logging, and testing explicit. SMTP can work, especially with existing Swoosh setups, but a REST API is often simpler across containers, jobs, and restricted runtimes.
Can I use Volanea with Phoenix and Oban?
Yes. A Phoenix controller, context, LiveView handler, or Oban worker can call the same Volanea email module. For important sends such as receipts, invitations, and notifications, enqueue work after the business transaction succeeds and use a bounded retry policy.
Should my Volanea API key go in config/prod.exs?
Do not hardcode an API key in source-controlled configuration. Use your deployment platform’s secret manager and load the value through runtime configuration. This keeps the same release artifact portable between environments and makes rotation safer.
Is SMTP available in edge runtimes?
Do not assume it is. Some edge platforms restrict raw TCP or SMTP ports; for example, Cloudflare Workers prohibit outbound TCP connections on port 25. A REST email API over HTTPS is the more portable option for edge-triggered and serverless workflows.
How do I avoid duplicate emails when a job retries?
Persist a stable notification or business-event ID before sending, and use it to prevent duplicate job execution where possible. Treat ambiguous timeouts carefully: they do not always prove the provider failed to accept the message. Build your workflow so support and reconciliation can identify a message attempt without blindly resending it.