Send email with Elixir by calling Volanea’s REST API from your application with a small, explicit HTTP client. This guide uses Req, reads your Volanea API key from an environment variable, and sends one transactional message with a copy-pasteable example.
What you will build
You will create a small Elixir script that sends a transactional email through Volanea. The integration uses the standard HTTPS API rather than an unofficial or provider-specific Elixir SDK, which keeps the implementation portable and makes every request detail visible in your code.
The finished example does four important things:
- Installs the
ReqHTTP client as a Mix dependency. - Reads
VOLANEA_API_KEYfrom the process environment instead of hard-coding a secret. - Sends a JSON
POSTrequest to Volanea’s email sending endpoint. - Handles both successful HTTP responses and transport-level failures.
This pattern works in a plain Mix project, a Phoenix application, a background job, or a release. The sending call is synchronous in the sample so it is easy to understand and test. In a production application, you can decide whether to make it part of a request flow, hand it to a job processor, or trigger it from a supervised workflow.
Before sending, make sure the from address uses a domain you have configured for sending in Volanea. A valid API key only authorizes the API request; it does not make an unverified sender identity usable.
Prerequisites
You need the following before running the example:
- Elixir and Erlang/OTP installed locally.
- A Volanea project and a secret API key.
- A sending domain configured in Volanea.
- Access to an inbox you can use as the test recipient.
- A new or existing Mix project.
Volanea’s single-send endpoint is POST https://api.volanea.com/v1/send. It accepts one recipient or multiple recipients, up to the endpoint’s documented limit. For this first send, use a single address you control. That makes it easier to distinguish an integration issue from recipient-side filtering, suppression, or mailbox rules.
Do not put a live secret key in source control, screenshots, client-side JavaScript, or a Phoenix LiveView rendered to a browser. The API key belongs only in a trusted server-side environment.
Install the Elixir dependency
There is no Volanea-specific Elixir package required for this guide. Instead, use Req, an Elixir HTTP client that can encode a map as JSON and decode JSON responses for you.
Create a Mix project if you do not already have one:
mix new volanea_elixir
cd volanea_elixir
Add Req to the deps function in mix.exs:
defp deps do
[
{:req, "~> 0.7.0"}
]
end
Then run this exact install command:
mix deps.get
Req handles JSON encoding when you pass the json: option. That is preferable to manually encoding a request body for a first integration because it also sets the JSON content type for the request.
If this code is going into an existing Phoenix project, add the dependency to that project’s existing deps list rather than creating a separate Mix project. Run mix deps.get after changing mix.exs, then restart the Phoenix server if it is currently running.
Set your Volanea API key securely
Export your key in the shell that will run the script:
export VOLANEA_API_KEY="sk_your_volanea_secret_key"
For a local test, verify that the variable exists without printing its value:
test -n "$VOLANEA_API_KEY" && echo "VOLANEA_API_KEY is set"
Use a secrets manager or your deployment platform’s encrypted environment-variable feature in staging and production. A key should be scoped and rotated according to your organization’s security policy. Treat it as a credential with permission to create outbound mail, not as ordinary application configuration.
A useful distinction is that environment variables solve secret injection, not secret lifecycle. You still need to decide who can create keys, where they are stored, how deployments receive them, how you rotate them, and how you revoke an exposed key. Keep those controls outside the application repository whenever possible.
Complete working example: send email with Elixir
Create a file named send_email.exs in the project root. Replace the example sender with an address on your configured sending domain and replace the recipient with an inbox you control.
# send_email.exs
Mix.install([
{:req, "~> 0.7.0"}
])
defmodule VolaneaEmail do
@send_url "https://api.volanea.com/v1/send"
def send_transactional_email do
api_key = System.fetch_env!("VOLANEA_API_KEY")
payload = %{
from: "Volanea Test <hello@your-verified-domain.com>",
to: ["you@example.com"],
subject: "Your Elixir email is working",
html: """
<h1>It works</h1>
<p>This transactional email was sent from Elixir through Volanea.</p>
"""
}
headers = [
{"authorization", "Bearer #{api_key}"},
{"accept", "application/json"}
]
case Req.post(@send_url,
headers: headers,
json: payload,
receive_timeout: 15_000
) do
{:ok, %{status: status, body: body}} when status in 200..299 ->
IO.puts("Email accepted by Volanea.")
IO.inspect(body, label: "Response")
:ok
{:ok, %{status: status, body: body}} ->
IO.puts("Volanea returned HTTP #{status}.")
IO.inspect(body, label: "Error response")
{:error, {:http_error, status, body}}
{:error, exception} ->
IO.puts("The request could not reach Volanea.")
IO.inspect(exception, label: "Transport error")
{:error, {:transport_error, exception}}
end
end
end
VolaneaEmail.send_transactional_email()
Run it with:
elixir send_email.exs
The Mix.install/1 call makes the standalone script copy-pasteable: Elixir downloads the Req dependency when needed. If you are integrating the same code into a normal Mix or Phoenix application, remove Mix.install/1 from the module file because the dependency belongs in mix.exs and is installed with mix deps.get.
The request has four essential parts:
authorizationcarries the secret key using Bearer authentication.acceptasks for a JSON response.json: payloadserializes the Elixir map as JSON and supplies the JSON request content type.receive_timeoutlimits how long the calling process waits for the remote response.
The sample prints the response body on success so you can retain the send identifier or other response data your application needs. In production, avoid logging recipients, rendered content, API keys, or other sensitive values unnecessarily. Log only the identifiers and fields needed to investigate an operational problem.
Understand the message payload
The payload in the example contains the minimum useful fields for an HTML transactional email:
%{
from: "Volanea Test <hello@your-verified-domain.com>",
to: ["you@example.com"],
subject: "Your Elixir email is working",
html: "<p>This transactional email was sent from Elixir through Volanea.</p>"
}
Sender address
The from value identifies the message sender. The email address portion should belong to a domain you have configured for sending. A display name is optional from an email-format perspective but is usually helpful for recipients because it provides recognizable context in the inbox.
Do not use a customer-provided email address as the sender unless your product is specifically designed and authorized to send on that identity. In most application-email flows, customer addresses belong in reply handling or message content, while the from address remains an authenticated address your organization controls.
Recipient list
The to field is an array in this example, even though it contains one recipient. Using an array makes the shape explicit and aligns well with a send endpoint that can accept more than one recipient.
For transactional mail, keep recipient selection deliberate. A password reset, verification link, receipt, or security alert should normally target the single user associated with the triggering event. Do not reuse a transactional-send path for promotional blasts simply because the API accepts multiple addresses.
Subject and HTML
The subject should state the message purpose clearly. For an account alert, receipt, or security event, direct wording makes the message easier to recognize and reduces support requests.
The html value is the rendered HTML body. Keep user-controlled values escaped before inserting them into HTML. If you interpolate untrusted data directly into a template string, you can create malformed markup or expose recipients to content you did not intend to send.
For example, do not build a message body with raw user input like this:
html = "<p>Hello #{params[\"name\"]}</p>"
Instead, escape dynamic content with the HTML-escaping facilities appropriate to your application or template engine before including it in markup. Phoenix applications can use their existing rendering and escaping conventions; a plain Elixir project should choose a deliberate templating approach instead of treating HTML strings as safe by default.
Run and verify your first send
Run the script only after exporting VOLANEA_API_KEY and replacing the sample addresses:
elixir send_email.exs
A successful API response means Volanea accepted the request for processing. It is not the same thing as a recipient opening the message, and it is not always proof that the message reached a mailbox. Email delivery has several stages: API acceptance, queueing, provider processing, recipient-server acceptance, and mailbox placement.
For a first test, verify at three levels:
- Application output: confirm the script prints an accepted response rather than an HTTP or transport error.
- Volanea activity: inspect the message or event information available for the send in your account.
- Recipient inbox: check the inbox, spam folder, and any mailbox rules for the test address.
If the API accepts the message but it is not visible in the inbox, do not immediately retry several times. First inspect the send result, the recipient address, sender-domain configuration, and any suppression state. Repeated retries can create duplicates when an earlier request actually succeeded but the client did not receive the response.
Use the code in a Mix or Phoenix application
A standalone .exs script is useful for validating credentials and sender setup. In an application, place the request code in a dedicated module rather than calling Req.post/2 throughout controllers, contexts, or LiveViews.
For example, create lib/my_app/volanea_mailer.ex:
defmodule MyApp.VolaneaMailer do
@send_url "https://api.volanea.com/v1/send"
def send_welcome_email(recipient_email) do
api_key = System.fetch_env!("VOLANEA_API_KEY")
Req.post(@send_url,
headers: [
{"authorization", "Bearer #{api_key}"},
{"accept", "application/json"}
],
json: %{
from: "My App <hello@your-verified-domain.com>",
to: [recipient_email],
subject: "Welcome to My App",
html: "<p>Thanks for creating an account.</p>"
},
receive_timeout: 15_000
)
end
end
This separation has practical benefits. It gives your application one place for authentication headers, API URL configuration, response handling, telemetry, retry policy, and tests. It also prevents a small email request from becoming duplicated in multiple product flows with inconsistent sender addresses or error behavior.
In Phoenix, do not make an external email request from browser-side code. Your browser must never receive the Volanea API key. Trigger mail from a controller, context, worker, or server-side process after the relevant business operation has completed.
For user-facing flows, consider what happens when email sending is slow or temporarily unavailable. A registration endpoint may create the account successfully while the welcome email fails. A password reset flow may need a stricter outcome. Define whether the email send is required for the request to succeed, whether it should be retried later, and what message the user sees in each case.
Handle failures and retries safely
The sample differentiates between two broad failure types:
- An HTTP response arrived, but Volanea returned a non-2xx status.
- The HTTP client could not complete the request, such as a timeout, DNS failure, TLS issue, or network interruption.
That difference matters. When you receive an HTTP error response, you have a concrete status and response body to inspect. When the request fails in transit, the remote service may or may not have received it. Blindly retrying every failure can create duplicate transactional emails.
Decide which failures are retryable
A missing key, malformed JSON request, invalid sender, or invalid recipient should be fixed in configuration or application code. Retrying the same invalid request will not make it valid.
A temporary network failure or rate-limit response may be retryable, but retries should be bounded and observable. Use a background job system for durable retries when the email is not required to complete the current web request. Preserve enough context to recreate the exact intended message, while avoiding unsafe persistence of unnecessary personal data.
Prevent duplicate sends
For events that must happen once, such as receipts or password-reset messages, record your application-level business event and sending intent. Associate a stable internal identifier with the action, such as an order ID plus message type, rather than treating the HTTP call itself as your only source of truth.
Volanea documents idempotency behavior for sending APIs. When you add an idempotency key to a retryable send flow, generate one value for the original logical send and reuse that same value only for retries of the identical request. Do not reuse a key for a changed message body or a different recipient.
A simple operational approach is:
- Create the business record first, such as an order or verification request.
- Persist an outbound-message record with a unique application identifier.
- Send from a worker and store the provider response or failure details.
- Retry only according to a defined policy.
- Use delivery events to improve visibility after API acceptance.
This approach is more reliable than putting an HTTP call inside a database transaction and assuming the remote API participates in that transaction. It does not.
Common errors
This section covers problems you are likely to see when you send email with Elixir through a REST API.
VOLANEA_API_KEY is missing
If System.fetch_env!("VOLANEA_API_KEY") raises an error, the environment variable was not available to the Elixir process. Export it in the same shell, configure it in your deployment environment, or load it through your established local-development configuration process.
Do not replace fetch_env! with a hard-coded fallback secret. Failing immediately is safer than accidentally sending with an old, exposed, or developer-specific credential.
Authentication failures
An HTTP 401 response generally indicates a missing, malformed, revoked, or invalid API key. Confirm that the key is a Volanea secret key, that it has not been truncated by shell quoting, and that the request sends it in the Authorization header as a Bearer token.
Also verify that you are using the intended project’s key. A key from one environment can be valid but not authorized for the sender domain, templates, or data you expect in another environment.
Wrong content type or invalid JSON body
The API expects a JSON request body. With Req, use json: payload; do not pass an Elixir map through body: and assume it will become JSON automatically.
This is correct:
Req.post(url, headers: headers, json: payload)
This is not a JSON request body:
Req.post(url, headers: headers, body: payload)
Using json: lets Req encode the map and set content-type: application/json. If you manually encode JSON instead, you are responsible for setting the content type yourself.
Invalid or unconfigured sender address
A request can fail validation when the from address is malformed or its domain is not configured for sending. Replace hello@your-verified-domain.com with an address on a domain you control and have set up in Volanea.
Avoid testing with a placeholder sender and assuming a delivery problem. Sender authorization is part of the initial integration, not a later deliverability optimization.
Recipient formatting problems
Use a real RFC-style email address for the recipient and keep it as a string inside the to list. Common mistakes include accidentally passing a map, an empty string, a display name without an address, or an address containing whitespace copied from a form field.
Normalize and validate application input before it reaches your mailer. For one-off tests, type the recipient directly rather than relying on unverified fixture data.
Treating a successful request as a delivered message
A 2xx response means the API accepted the send request. It does not guarantee that the recipient saw the message in the primary inbox. Delivery can still be affected by recipient-server response, mailbox filtering, recipient suppression, or the recipient address itself.
Use Volanea’s message activity and webhook events to observe what happens after the send call. Do not build product logic that assumes an API acceptance response is an email open, click, or completed user action.
Async and task mistakes in Elixir
The Req.post/2 call in this guide runs in the calling process. It is not JavaScript-style async/await, and it does not return a promise. If you wrap sending in Task.async/1, you must still call Task.await/2 or otherwise supervise and observe the task result.
This pattern waits for the send result:
task = Task.async(fn -> MyApp.VolaneaMailer.send_welcome_email("you@example.com") end)
Task.await(task, 20_000)
Do not start an unlinked, unobserved task for important transactional mail and then discard the result. If the task crashes or the process exits, you may lose failure visibility. For durable delivery work, use a supervised job system and persist the work item before attempting the remote request.
Timeouts and duplicate retries
A timeout means your application did not receive a response within the configured period. It does not prove that Volanea did not receive the request. Before retrying, use your own send records and Volanea’s response or activity information where available.
For messages where duplicates are harmful, use a stable idempotency strategy and retry the same logical request rather than creating a fresh send operation every time a network call is uncertain.
Production considerations
A first successful email is an integration milestone, not the end of the implementation. Production mail needs clear ownership, sender-domain authentication, event handling, sensible retry behavior, monitoring, and separation between transactional and campaign use cases.
Keep transactional email close to the triggering event
Transactional email should correspond to a specific user or system action: a verification email, password reset, receipt, security notification, invitation, or account update. Model that relationship in your application data so support teams can answer questions such as whether an order receipt was requested, accepted, retried, or suppressed.
A durable event record also makes it easier to correct operational mistakes. If you need to resend a receipt, you can create an intentional new send rather than hoping an old web-request log contains enough information to replay safely.
Add observability without leaking content
Log the HTTP status, an internal message type, an internal event ID, and provider response identifiers where appropriate. Avoid storing API keys, full message bodies, access tokens, password-reset links, or recipient data in general-purpose logs.
Metrics can track attempted sends, accepted sends, HTTP failures, transport failures, retry counts, and processing latency. Alerts should focus on abnormal failure rates or sustained delivery degradation rather than a single ordinary invalid-address error.
Test the integration
Keep at least one integration test path that confirms your request builder creates the expected URL, headers, and JSON payload without exposing real credentials. For unit tests, mock or stub the HTTP boundary. For staging tests, use a controlled recipient and a configured staging sender domain.
Test failure behavior as carefully as the happy path. Confirm what happens when the API key is missing, the sender is invalid, the recipient is malformed, the network times out, or Volanea returns an error status. Your product should fail predictably even when mail infrastructure is unavailable.
Next steps
Once the basic send works, move beyond a hard-coded HTML string.
First, use templates for recurring message types such as welcome emails, receipts, and account notifications. Templates let your application identify a reusable message design and provide only the message-specific data needed for a send. This reduces duplicated HTML across your codebase and makes controlled content changes easier. Review the email API reference and setup guides before wiring a template into production.
Second, configure webhooks so your application can receive email lifecycle events after Volanea accepts a message. Webhooks are useful for recording delivery outcomes, bounces, complaints, and other downstream events in your own systems. Verify webhook signatures, make handlers idempotent, return a response quickly, and process longer work asynchronously.
Finally, revisit your sender-domain setup and your application’s retry policy before increasing volume. Reliable transactional email depends on more than a successful POST: the sender identity, recipient quality, message purpose, observability, and failure handling all affect the system your users experience.
FAQ
Do I need an official Volanea Elixir SDK?
No. This guide uses Volanea’s REST API with the Req HTTP client. That avoids depending on a language-specific SDK and keeps the request format explicit in your Elixir code.
What is the install command for this integration?
After adding {:req, "~> 0.7.0"} to mix.exs, run:
mix deps.get
For the standalone script, Mix.install/1 can install the dependency automatically when you run elixir send_email.exs.
Why is my email request accepted but not in the inbox?
API acceptance is only the first stage of email processing. Check the sender-domain configuration, recipient address, suppression status, Volanea message activity, spam folder, and recipient mailbox rules before retrying.
Should I send email directly from a Phoenix controller?
You can make the server-side call from a controller, but a dedicated mailer module is cleaner. For non-critical notifications, a background job often gives you better retry handling and keeps the web response fast. Never send directly from browser-side code because that would expose your API key.
Can I retry a failed transactional send?
Yes, but distinguish validation errors from transient failures. Retry only when your policy says it is safe, record the logical send in your application, and use idempotency for retries so an uncertain network response does not become duplicate email.