Send email with Remix through a server-side route so your Volanea API key never reaches browser code. This guide uses Volanea’s REST send endpoint, a Remix action, and Node’s built-in fetch to submit one transactional email as JSON.
The integration deliberately uses the REST API rather than a fictional framework-specific package. That keeps the sending layer portable: Remix owns request handling, environment variables hold the secret, and Volanea receives a standard authenticated HTTP request. It also means the same email function can later be reused by a signup flow, password-reset workflow, background worker, or queue consumer.
What you will build
You will add a Remix resource route that accepts an HTTP POST request and sends one transactional message through Volanea. The finished route:
- Loads
VOLANEA_API_KEYfrom a server-side environment variable. - Sends JSON to
https://api.volanea.com/v1/send. - Uses an authenticated
Authorizationheader andContent-Type: application/json. - Sends an email with a verified sender, one recipient, subject line, and HTML body.
- Returns Volanea’s response as JSON so you can inspect the result while testing.
- Adds an
Idempotency-Keyheader to identify the individual send attempt.
This is a server-only integration. Do not put the API request in a Remix component, browser event handler, client loader, or public environment variable. Any secret that is bundled into browser JavaScript can be copied by anyone visiting the page.
Volanea’s send endpoint accepts a single message addressed to one recipient or multiple recipients, up to 50 addresses for one send request. For the first test, use only your own inbox. A successful API response indicates that Volanea accepted the request for processing; it should not be treated as proof that the recipient has opened the message or that an inbox provider has placed it in the inbox.
Prerequisites
Before you send email with Remix, make sure the following are ready:
- A Remix application running on a Node.js runtime.
- Node.js 18 or later, which provides the server-side
fetchAPI used in this guide. - A Volanea secret API key, stored only in server-side configuration.
- A sending domain configured and verified in Volanea.
- An address on that verified domain to use as the
fromvalue. - A safe test recipient that you control.
The most common setup failure is starting with an unverified sender address. Your from address should use the domain you have authenticated for sending. Replace the example sender before testing; do not leave hello@your-verified-domain.com in production code.
You also need a clear boundary between configuration that belongs on a developer machine and configuration that belongs in deployment. A local .env file is useful for development, but it is not a mechanism for passing secrets to production. Configure VOLANEA_API_KEY in your host’s encrypted environment-variable or secret-management system when you deploy.
Install the dependency
This guide uses the REST API directly, so there is no Volanea-specific SDK to install or maintain. Remix already includes its own server utilities, and Node 18+ includes fetch. Install dotenv only to load a local .env file when running the Node-based Remix app locally:
npm install dotenv
The request itself is made with native fetch. This is intentional: it avoids inventing a Send With Remix SDK method and keeps the code aligned with Volanea’s REST API.
If your project already has a different environment-loading convention, you can omit dotenv and retain the rest of the route unchanged. For example, many deployment platforms inject VOLANEA_API_KEY directly into process.env at runtime.
Why native fetch is a good fit here
A transactional email send is a straightforward server-to-server HTTP request. Using fetch gives you direct control over:
- The endpoint URL.
- The bearer token header.
- JSON encoding.
- The idempotency key.
- HTTP error handling.
- The raw API response available for logs and diagnostics.
It also prevents a dependency from hiding important behavior such as retries, serialization, or error conversion. That visibility matters for email because an ambiguous network timeout can leave you unsure whether a provider received the request. Volanea supports the Idempotency-Key header for safe retry patterns; later in this guide, you will see how to use that correctly in a production workflow.
Add your environment variables
Create a .env file at the root of your Remix project for local development:
VOLANEA_API_KEY=sk_test_replace_with_your_key
VOLANEA_FROM=Acme <hello@your-verified-domain.com>
Use a test key while you are wiring up the route when one is available for your account. Before production, replace it with the appropriate live secret in your deployment environment. Never commit .env to source control.
Add the file to .gitignore if it is not already ignored:
.env
.env.*
The VOLANEA_FROM variable is optional from a technical perspective because you could place the sender in code. Keeping it in configuration is safer operationally: staging, preview, and production environments can use distinct approved senders without changing application source.
Keep server variables separate from browser variables
Remix runs code in more than one place. Route components render application UI, while actions and loaders execute on the server. A secret should be read only inside server-side files and server-side functions.
Do not use a public-prefix convention for VOLANEA_API_KEY, do not return it from a loader, and do not expose it through a global window.ENV object. The browser does not need the key. It only needs to submit a form or call your own application endpoint; the Remix server performs the authenticated Volanea request.
Create the Remix resource route
Create this file:
app/routes/send-email.ts
Paste in the complete route below. It is a resource route: it does not render a page. It handles a POST, sends the message on the server, and returns JSON.
import "dotenv/config";
import { randomUUID } from "node:crypto";
import { json } from "@remix-run/node";
const VOLANEA_SEND_URL = "https://api.volanea.com/v1/send";
function parseResponseBody(text: string) {
if (!text) return null;
try {
return JSON.parse(text);
} catch {
return text;
}
}
export async function action() {
const apiKey = process.env.VOLANEA_API_KEY;
const from = process.env.VOLANEA_FROM;
if (!apiKey) {
return json(
{
error: "Missing VOLANEA_API_KEY. Add it to your server environment before sending email.",
},
{ status: 500 },
);
}
if (!from) {
return json(
{
error: "Missing VOLANEA_FROM. Use an address on a verified sending domain.",
},
{ status: 500 },
);
}
const recipient = "you@example.com";
try {
const volaneaResponse = await fetch(VOLANEA_SEND_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(),
},
body: JSON.stringify({
from,
to: [recipient],
subject: "Hello from Remix",
html: "<strong>It works!</strong><p>This transactional email was sent from a Remix action.</p>",
}),
});
const responseText = await volaneaResponse.text();
const responseBody = parseResponseBody(responseText);
if (!volaneaResponse.ok) {
return json(
{
error: "Volanea rejected the email request.",
status: volaneaResponse.status,
details: responseBody,
},
{ status: volaneaResponse.status },
);
}
return json(
{
message: "Volanea accepted the email request.",
result: responseBody,
},
{ status: 200 },
);
} catch (error) {
console.error("Unable to send email through Volanea", error);
return json(
{
error: "The Remix server could not reach Volanea.",
},
{ status: 502 },
);
}
}
Change this line before using the route:
const recipient = "you@example.com";
Replace it with an address you control. Keep the HTML deliberately simple for the first request. Once the request succeeds, you can move the recipient, subject, and content into data from your own application.
What the route does
The route begins by importing dotenv/config, which loads local environment variables before the action reads them. The randomUUID import creates a unique idempotency key for this distinct send attempt.
The action validates configuration early. This is better than allowing an empty header or a malformed sender to reach the API because the error returned to your local terminal or API client identifies the real issue immediately.
The fetch call sends four important pieces of request metadata:
method: "POST"selects Volanea’s send endpoint.Authorization: Bearer ...authenticates the request with your secret key.Content-Type: application/jsontells the API to parse the request body as JSON.Idempotency-Keylabels this send operation with a unique value.
The JSON body contains the minimum meaningful transactional message: a sender, recipient array, subject, and HTML content. The response parser intentionally accepts both JSON and non-JSON response bodies, which makes failure diagnostics more useful if an upstream proxy or unexpected error returns plain text.
Run the route and send one message
Start your Remix application using the script defined by your project. In a typical app, that is:
npm run dev
Then submit a POST request to the new resource route from a second terminal:
curl -i -X POST http://localhost:3000/send-email
If your Remix development server uses another port, substitute that port in the command. A successful request returns JSON from the route, including the response Volanea returned. Check the test inbox after the request is accepted.
Do not test this route by visiting /send-email in the browser address bar. Browser navigation uses GET, while the route intentionally sends email only from POST. This distinction prevents accidental sends triggered by previews, crawlers, browser refreshes, or a user copying a URL.
Test with a form instead of curl
A form is often the right UI entry point for a real application. The email send must still occur in the action, not in browser code. Here is a small example that submits to the resource route:
import { Form } from "@remix-run/react";
export default function SendTestEmailButton() {
return (
<Form method="post" action="/send-email">
<button type="submit">Send test email</button>
</Form>
);
}
This component contains no API key and performs no direct request to Volanea. It posts to your Remix server, where the resource route owns the authentication and send operation.
For production user flows, avoid building a general-purpose public endpoint that always sends to a hard-coded recipient. Instead, perform the email send as part of a protected application action: after creating an account, generating a password-reset token, confirming a purchase, or creating an invitation. Validate the authenticated user and the destination address before constructing the request.
Understand the email payload
The example payload is intentionally small:
{
from,
to: [recipient],
subject: "Hello from Remix",
html: "<strong>It works!</strong><p>This transactional email was sent from a Remix action.</p>",
}
Each property represents a distinct delivery decision.
Sender: from
The sender identifies the mailbox and display identity recipients see. Use an address on a verified sending domain. A display-name format such as Acme <hello@example.com> is readable for people, but the domain must be one you have configured for sending.
Treat the sender as operational configuration, not user input. Letting users choose an arbitrary from address can cause authentication failures, spoofing concerns, and confusing replies. If your application supports multiple brands, use an allowlist of approved sender identities stored on the server.
Recipients: to
The to field is an array. Even for one message, use an array containing one valid address. This creates a predictable shape when you later add a small group of recipients.
That does not mean a transactional endpoint should become a campaign tool. A password reset, receipt, login alert, or invitation is normally specific to one person. Keep recipient selection tied to application data and apply authorization checks before sending.
Subject and HTML
The subject should describe the user’s action or account event. Avoid vague subjects such as “Notification” when a precise one such as “Reset your Acme password” is possible.
The html property holds the message body. Start with small, semantic HTML and test it in the inboxes your users actually use. Email clients do not behave like modern browsers: they can remove CSS, alter layout, block images, and render unsupported HTML differently. For important transactional flows, provide clear content even if images or styles are unavailable.
Use idempotency correctly in production
The sample creates a new UUID for every request because it demonstrates one independent test email. That is correct for a one-off manual test, but it is not enough for retry-safe business events.
When a network connection fails after Remix submits the request, your server may not know whether Volanea received it. Blindly repeating the operation with a new idempotency key can create two sends. To make a retry safe, reuse the same key for the same logical message.
For example, a receipt could derive an idempotency value from the immutable order identifier:
const idempotencyKey = `receipt-order-${order.id}`;
A password-reset email should use a unique reset-token or reset-event identifier, not only a user ID. A user may legitimately request several reset messages over time, but retries of one reset event should remain one operation.
A useful production pattern is:
- Create the business record or event in your database.
- Store a stable idempotency key with that event.
- Send using that key.
- If the network request is ambiguous, retry using the exact same key.
- Record the provider response and delivery-related events separately.
Do not generate a random key inside each retry loop. A random value identifies a new operation every time, which defeats deduplication.
Common errors
Authentication failures
If Volanea returns an authentication or authorization error, first confirm that VOLANEA_API_KEY is available to the Remix server process. Logging the full key is unsafe; instead, log whether the variable exists and, if necessary, only a non-sensitive prefix.
Check these items:
- The key was copied completely and has no accidental quotes or whitespace.
- The deployment environment has the variable configured, not only your local
.envfile. - The request uses
Authorization: Bearer ${apiKey}. - You are not attempting to read the key from browser code.
- The key type is appropriate for the environment you are testing.
A common Remix mistake is setting a variable in a shell after the development server has already started. Stop and restart the process after changing .env so the server receives the new configuration.
Wrong Content-Type or invalid JSON
The request body must be serialized with JSON.stringify, and the header must be exactly Content-Type: application/json. Sending a JavaScript object directly as body does not serialize it correctly. Sending form data or setting text/plain can cause the API to reject or misinterpret the payload.
Correct:
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
from,
to: [recipient],
subject: "Hello from Remix",
html: "<strong>It works!</strong>",
}),
Incorrect:
body: {
from,
to: [recipient],
}
The incorrect example is not valid for fetch and will not produce the JSON request Volanea expects.
Forgetting await fetch(...)
fetch is asynchronous. If you forget await, your action may return before you inspect the API response, and errors can be lost or appear as unhandled promise rejections.
Correct:
const volaneaResponse = await fetch(VOLANEA_SEND_URL, options);
Also await response-body reads such as await volaneaResponse.text() or await volaneaResponse.json(). An HTTP response object does not itself contain an already-parsed JSON body.
Sending from a browser component
A client-side call exposes the credential or forces you to create a weak public endpoint. A Remix component can submit a form, but it must not own the Volanea authorization header. Keep fetch(VOLANEA_SEND_URL, ...) in an action, loader used only for safe server work, background job, or other server-side module.
The action in this guide is preferable to a loader because it sends a side effect. Loaders commonly serve data for page rendering and may run on navigation or revalidation. Sending email from a loader risks accidental duplicate messages.
Using GET for a send operation
If you receive a 405 Method Not Allowed response from your own application, ensure you use POST with curl or a Remix <Form method="post">. A GET request should not send email.
The route in this guide exports only action, so it is intentionally not a page-view endpoint. This protects your test send against accidental browser navigation.
Sender-domain failures
If the API accepts the authentication but rejects the sender, verify that VOLANEA_FROM uses a configured sending identity. The local part of the address can vary according to your domain configuration, but the domain must match an authenticated domain you control.
Do not use a consumer mailbox, a customer-entered address, or an arbitrary domain as the sender. Use a controlled address such as support@yourdomain.com or notifications@yourdomain.com, then set an appropriate reply-to workflow in your broader messaging design when needed.
Recipient data is malformed or untrusted
The to field should always contain validated email addresses. In a real route, do not accept an arbitrary destination from an unauthenticated request and send directly. That can turn your application into an email relay.
Resolve recipients from trusted account data, validate any new address before using it, and require authorization for actions such as invitations or account alerts. You can also use Volanea’s email address verification tool as part of an address-quality workflow before high-value sends.
Treating acceptance as final delivery
A successful send response confirms that the provider accepted the request. It does not guarantee a message was delivered to the recipient’s mail server, placed in the inbox, opened, or clicked. Bounces, complaints, recipient suppression, and mailbox-provider decisions happen later in the lifecycle.
Build your application so the immediate send response records an attempted message, while asynchronous delivery events update its final state. This distinction prevents support teams from promising “delivered” based solely on a successful API call.
Make the route production-ready
The copy-paste route is designed to prove the integration with one message. Production code should make the route specific to a real application event and should not let a visitor trigger arbitrary sends.
Move message values into trusted server logic
A password-reset action might generate a token, save it to the database, and then send a link to the email already associated with the account. A receipt action might load an order by ID and send to the purchaser stored on that order. In both cases, the server decides the recipient, sender, subject, and content.
Do not trust a form field for the sender, raw HTML, or recipient list unless your product explicitly requires it and applies strong validation, authorization, and abuse controls. The more email content comes from user input, the more important it becomes to escape untrusted values before placing them in HTML.
Add observability without leaking content
Log enough information to investigate failures without logging secret keys or full email bodies. Useful fields often include:
- Your internal event or order ID.
- A stable idempotency key.
- The response status.
- A provider message identifier, when returned.
- A redacted recipient identifier or internal user ID.
- The time the request was attempted.
Avoid logging API keys, password-reset URLs, access tokens, raw personalization data, or full recipient lists. Transactional email often carries sensitive account context, and logs typically have wider access and longer retention than the application database.
Queue non-interactive sends
For workflows where a user does not need an immediate result, consider writing the event to a queue and letting a worker call Volanea. This isolates email provider latency from the main request and gives you a controlled retry policy.
For example, checkout can commit an order first, enqueue a receipt event, and return the confirmation page. The worker then sends the receipt with the order-based idempotency key. If a provider request fails transiently, the worker retries the same logical event rather than asking the customer to repeat checkout.
This does not mean every send must be asynchronous. Password reset and sign-in verification messages are user-facing and time-sensitive, so an immediate server-side request can be appropriate. In either design, preserve one stable identity for one intended email event.
Next steps
Once the one-message test works, move from hard-coded HTML to maintainable messaging infrastructure.
First, use reusable templates for transactional messages that share a structure across events, such as receipts, invitations, account verification, and password resets. Keep business data in your Remix application, pass only the intended variables for each message, and test each template with realistic values. Volanea’s API documentation and setup material is available in the email API reference and setup guides.
Second, add webhooks so your application can receive message lifecycle events after the initial send request. Webhooks let your server react to events such as delivery outcomes, bounces, complaints, and engagement where applicable. Verify webhook signatures, preserve the raw request body when your verification method requires it, return a quick success response, and process heavier database work asynchronously.
Third, replace the fixed recipient with data from a protected application workflow. A good first production use case is a signed-up confirmation message or account invitation, because it has a clear event, a known recipient, and a natural idempotency value.
FAQ
Do I need a Volanea SDK to send email with Remix?
No. This guide uses Volanea’s REST endpoint with Node’s built-in fetch, so there is no framework-specific SDK method to learn. The only installed package is dotenv for loading a local .env file during development.
Should I send email from a Remix loader or action?
Use an action for a send operation initiated by POST. Email sending changes external state, while loaders are generally intended to load data for rendering. An action also makes it easier to prevent accidental sends caused by page loads or refreshes.
Why is the Volanea API key not in the React component?
React components can be sent to the browser. A Volanea secret key must remain on the server, so the authenticated request belongs in a Remix action, server utility, or background worker.
What should I use for the idempotency key?
Use one stable value for one logical email event, such as an order receipt ID or a password-reset event ID. Reuse that same value only if you are retrying the same send after an ambiguous failure; use a new value for a genuinely new email.
Does a successful API response mean the email reached the inbox?
No. It means the send request was accepted by Volanea. Delivery, bounce, complaint, inbox placement, opening, and clicking are later events. Use webhooks and message-event handling to track those outcomes separately.