Send email with AWS Lambda when an application event needs an immediate, server-side transactional message: a password reset, receipt, verification link, alert, or invitation. This guide deploys a Node.js Lambda function that calls Volanea’s POST /v1/send endpoint over HTTPS and sends one email using credentials stored outside the source code. Volanea’s send endpoint supports sending a single message to one address or a recipient list. (volanea.com)
What you will build
You will create a small Node.js project, install one HTTP dependency, package it for AWS Lambda, configure environment variables, and invoke a handler that sends a transactional message through the Volanea REST API.
The finished function has these characteristics:
- It uses
axiosto make the HTTPS request. - It reads the Volanea API key from
VOLANEA_API_KEY. - It reads the verified sender address from
VOLANEA_FROM. - It reads a test recipient from
VOLANEA_TO. - It sends both HTML and plain-text message content.
- It checks the HTTP response before reporting success.
- It logs safe diagnostic data to CloudWatch without printing the API key.
This is intentionally a REST integration rather than a fictional Lambda-specific SDK. Lambda invokes your JavaScript handler; the handler makes a standard authenticated HTTP request to Volanea. That makes the code portable to an API Gateway route, EventBridge target, SQS consumer, scheduled job, or any other Lambda trigger.
How the Lambda email flow works
A Lambda function is short-lived application code that runs in response to an AWS event. For email delivery, your trigger might be an account signup, completed order, failed payment, password-reset request, or a queue message created by another service.
The flow in this guide is:
- AWS invokes
index.handler. - The function loads its environment configuration through
process.env. - The function builds a JSON request body containing
from,to,subject,html, andtext. axiossends an HTTPSPOSTrequest tohttps://api.volanea.com/v1/send.- Volanea accepts or rejects the request.
- The function returns a successful invocation result only after it receives a successful HTTP response.
AWS Lambda passes the event object to the exported handler, and Node.js Lambda functions can be written as asynchronous handlers. AWS also sends function output to the function’s CloudWatch Logs log group. (docs.aws.amazon.com)
A successful API response means the provider accepted the request for processing. Do not equate acceptance with inbox placement: mailbox-provider filtering, recipient-server behavior, bounces, and complaints occur after the initial API request. In production, pair send requests with delivery-event processing and application-level state so that your system can distinguish “send request accepted” from “delivered,” “bounced,” or “complained.”
Prerequisites
Before packaging the function, prepare the sending identity and credentials that the request needs.
1. A Volanea API key
Create a Volanea secret API key and keep it server-side. The API reference identifies secret keys as sk_… or sk_test_… values and lists https://api.volanea.com as the base URL. (volanea.com)
Never put the key in browser code, a mobile application, a public repository, a Lambda test event, or a client-visible API response. Anyone with a usable sending key may be able to make authenticated requests as your account.
2. A verified sending domain and sender address
Set VOLANEA_FROM to an address at a domain you have verified in Volanea, such as Acme Notifications <notifications@example.com>. The sender must belong to a verified domain unless you are using the account’s test-mode behavior. (volanea.com)
Use a stable sender address for a message stream. For example, password resets might come from security@example.com, while order receipts come from orders@example.com. Avoid changing the visible sender arbitrarily between messages because recipients, support teams, and mailbox providers benefit from consistent identity.
3. Node.js and npm
This guide uses a Node.js Lambda runtime and npm. AWS currently documents managed Node.js 22 and Node.js 24 Lambda runtimes; this example uses Node.js 22.x so the deployment configuration and code are explicit. (docs.aws.amazon.com)
4. An AWS Lambda function
Create a Lambda function with the Node.js 22.x runtime and configure its handler as index.handler. AWS’s Node.js documentation uses index.handler for a source file named index.mjs exporting handler. (docs.aws.amazon.com)
If your Lambda runs inside a private VPC, make sure it has a valid outbound path to the public internet. A function attached only to private subnets without appropriate egress will not be able to reach the Volanea HTTPS API.
Create the Lambda project and install the dependency
Create a new directory locally. The following command initializes the package and installs axios, the only external dependency used by the function:
mkdir volanea-lambda-email
cd volanea-lambda-email
npm init -y
npm install axios
The exact install command is:
npm install axios
Using axios keeps the request code clear and works in a packaged Node.js Lambda deployment. Node.js also includes fetch in modern runtimes, but this guide installs an explicit dependency so the project is reproducible and the HTTP behavior is visible in package.json.
Update package.json to mark the project as an ES module. The Lambda code below uses import syntax and a .mjs entry file.
{
"name": "volanea-lambda-email",
"version": "1.0.0",
"private": true,
"type": "module",
"dependencies": {
"axios": "^1.8.0"
}
}
You do not need to install the AWS SDK for this example. The function is not calling an AWS service from application code; it is making an outbound HTTPS request to Volanea. The Lambda execution role still needs the standard permissions required by your trigger and logging configuration, but sending through the Volanea REST API does not require a special AWS email permission.
Complete working AWS Lambda code sample
Create a file named index.mjs in the project directory. Replace the example sender and recipient only through Lambda environment variables, not by hard-coding credentials into the file.
import axios from "axios";
const apiKey = process.env.VOLANEA_API_KEY;
const from = process.env.VOLANEA_FROM;
const to = process.env.VOLANEA_TO;
if (!apiKey) {
throw new Error("Missing required environment variable: VOLANEA_API_KEY");
}
if (!from) {
throw new Error("Missing required environment variable: VOLANEA_FROM");
}
if (!to) {
throw new Error("Missing required environment variable: VOLANEA_TO");
}
const volanea = axios.create({
baseURL: "https://api.volanea.com",
timeout: 10_000,
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
Accept: "application/json"
},
validateStatus: () => true
});
export const handler = async () => {
const email = {
from,
to: [to],
subject: "Welcome to Acme",
text: "Welcome to Acme. Your account is ready.",
html: `
<!doctype html>
<html lang="en">
<body>
<h1>Welcome to Acme</h1>
<p>Your account is ready.</p>
</body>
</html>
`
};
const response = await volanea.post("/v1/send", email);
if (response.status < 200 || response.status >= 300) {
console.error("Volanea send request failed", {
status: response.status,
data: response.data
});
throw new Error(`Volanea send request failed with HTTP ${response.status}`);
}
console.log("Volanea send request accepted", {
status: response.status,
data: response.data
});
return {
statusCode: 200,
body: JSON.stringify({
ok: true,
message: "Transactional email request accepted",
result: response.data
})
};
};
The request uses the documented Volanea send route, POST /v1/send. (volanea.com) The from value is the sender identity, to contains one recipient in this example, and the message includes a subject plus HTML and text alternatives.
Keep the client creation outside the handler. Lambda may reuse a warm execution environment for later invocations, so the initialized HTTP client can be reused instead of being rebuilt for every event. The message payload itself stays inside the handler because a real transactional email should be constructed from the current event, not from data retained from a prior invocation.
Why include both HTML and text?
HTML provides the formatted experience, while the text version is a useful fallback for recipients and mail clients that do not render HTML. Keep their meaning aligned. If the HTML says an account is ready but the text version says a password reset is required, users may be confused and security investigations become harder.
For real application content, escape or safely encode any untrusted user-provided value before adding it to HTML. A customer’s name, support ticket title, or product name may contain characters that alter markup if interpolated directly into an HTML string.
Configure Lambda environment variables
Set the following environment variables on the function:
| Variable | Example value | Purpose |
|---|---|---|
VOLANEA_API_KEY | sk_... | Secret key used in the Authorization header. |
VOLANEA_FROM | Acme Notifications <notifications@example.com> | A sender address on a verified sending domain. |
VOLANEA_TO | developer@example.net | A safe test recipient for the first invocation. |
In the Lambda console, AWS documents this path: open the function, select Configuration, select Environment variables, then use Edit and Add environment variable. (docs.aws.amazon.com) Use those names exactly because the code reads process.env.VOLANEA_API_KEY, process.env.VOLANEA_FROM, and process.env.VOLANEA_TO.
Environment variables are made available to Lambda code at runtime, but AWS recommends AWS Secrets Manager for sensitive values such as API keys and authorization tokens. (docs.aws.amazon.com) For a first integration, an encrypted Lambda environment variable can be practical. For a production environment with rotation requirements, centralized secret access controls, or multiple services, retrieve the API key from Secrets Manager and cache it carefully for the lifetime of the Lambda execution environment.
Do not write this in a Lambda test event:
{
"apiKey": "sk_put_your_real_key_here"
}
Lambda test events can be viewed by people with access to the function configuration. Keeping the secret in an environment variable or a secrets service reduces accidental disclosure and makes key rotation possible without changing the code package.
Package and deploy the function
Lambda needs your application file and installed production dependencies. From the directory containing index.mjs, package.json, and node_modules, create a ZIP deployment package:
zip -r function.zip index.mjs package.json node_modules
Upload function.zip to the Lambda function, then set:
Runtime: Node.js 22.x
Handler: index.handler
If you use infrastructure as code, the essential configuration is the same: deploy the package, select a supported Node.js runtime, define the handler, and provide the environment variables. The Lambda runtime identifies the handler as the exported function AWS calls for each invocation. (docs.aws.amazon.com)
For a production deployment pipeline, do not create a ZIP on a developer laptop and upload it manually as the long-term process. Build in CI, run tests, generate a deterministic artifact, deploy through your normal AWS change process, and separate development, staging, and production keys. Manual deployment is reasonable for confirming the initial integration, but it is difficult to audit and reproduce at scale.
Invoke and test the email send
The example handler does not require an input event because the first goal is to prove the sending path works. In the Lambda console, create a minimal test event:
{}
Invoke the function. A successful invocation should return a JSON response whose ok value is true. The exact response data depends on the Volanea API response, so treat result as diagnostic output rather than a schema to hard-code against.
Then verify these three places:
- Lambda invocation result: confirms the handler completed without throwing.
- CloudWatch Logs: confirms the HTTP status and response diagnostic output were logged.
- The recipient inbox: confirms that the test recipient received the message.
Use an address you control for early tests. Check spam and junk folders before assuming a failure. If the message is accepted by the API but does not arrive, inspect the sender-domain setup, recipient address, suppression state, and subsequent delivery events.
Once the fixed test works, replace the static recipient pattern with data from a trusted application event. For example, an API Gateway handler might validate a signup request before calling a separate email-sending function, while an order system might place a receipt job on SQS and have a consumer Lambda send the receipt after the order transaction is committed.
Turn the example into a transactional-email function
A transactional email should be caused by a durable business event, not merely by a frontend action. If a browser calls an endpoint twice because of a network retry, or a user double-clicks a button, you do not want two password-reset messages or duplicate receipts.
A reliable design commonly separates the business transaction from delivery:
- Persist the business change, such as creating an order or generating a reset token.
- Persist an outbound-email record or enqueue an email job with a stable business identifier.
- Invoke the Lambda from the queue or event source.
- Record that the provider accepted the send request.
- Process later delivery events and update operational status.
This structure gives you an audit trail and a controlled retry point. A raw HTTP timeout is ambiguous: Volanea may have received the request even if Lambda did not receive the response. Do not blindly retry every failed POST from the handler without deciding how your application will prevent duplicate messages.
Passing trusted event data
When you adapt the sample, validate the event before using it. Keep the API key and sender address in configuration, while allowing only expected per-message values to come from an event. For example, a queue message might safely provide a recipient, an order number, and a pre-rendered amount, but should not be able to choose an arbitrary from identity or submit arbitrary HTTP headers.
Keep sensitive data out of subjects, URLs, and logs. Email subjects are often visible in notification previews and mailbox indexes. Password reset emails should contain a short-lived reset link or code, but they should not expose raw credentials, access tokens, payment details, or internal exception messages.
Use a real HTML-email layout
The code sample uses minimal HTML so you can verify the API integration. Production email markup has different constraints from web-page markup. Keep layouts simple, use inline-compatible styling where necessary, include a plain-text alternative, and test important messages in the mail clients your customers use.
Do not accept arbitrary HTML from a public request and send it directly. If your product has an administrator-authored notification feature, sanitize the allowed markup and separate template authoring from transaction data. That protects recipients and avoids using your sending system as an HTML injection relay.
Common errors
401 or 403 authentication failures
An authentication failure usually means the VOLANEA_API_KEY value is missing, invalid, revoked, copied with whitespace, or sent in the wrong header format. The sample sends the key in:
Authorization: Bearer sk_...
First, confirm that the environment variable exists on the deployed Lambda version. Then confirm that the code has been deployed after any configuration change. Do not log the full key while debugging. At most, log whether the variable is present and, if your security policy permits, a non-sensitive prefix length check.
A common local-development mistake is setting VOLANEA_API_KEY in a shell but forgetting that Lambda has its own separate environment-variable configuration. The local shell value does not automatically appear in AWS.
400 or 422 request-validation failures
These responses generally indicate that the JSON body does not meet the endpoint’s requirements. Check the sender, recipient, subject, and content fields first. Ensure from uses an allowed sender address, to contains a valid recipient address, and the request body is JSON rather than a string that looks like JSON.
The sample explicitly sets:
Content-Type: application/json
Do not use application/x-www-form-urlencoded, multipart/form-data, or a plain-text body for this endpoint. Also do not send an object through a client configuration that serializes it unexpectedly. axios.post("/v1/send", email) serializes the JavaScript object as JSON when used with the supplied JSON content type.
Wrong or unverified sender domain
If the from address is not associated with a verified domain, the provider can reject the request or restrict sending depending on the account state. Use a sender address you have already authenticated in Volanea, and keep the VOLANEA_FROM value identical across the function configuration and your sending-domain setup. The Volanea API documentation notes that the from address must belong to a verified domain outside test mode. (volanea.com)
Do not substitute the recipient’s domain, a personal mailbox, or a domain you do not control as the sender. Domain authentication is part of establishing a trustworthy sending identity.
Missing await or returning before the request finishes
The handler must await the HTTP request:
const response = await volanea.post("/v1/send", email);
This is not equivalent to starting the promise and immediately returning:
// Incorrect: do not do this.
volanea.post("/v1/send", email);
return { statusCode: 200, body: "queued" };
The incorrect version can report success before the HTTP request has completed, and errors may be lost or become difficult to associate with the business event. Use an async handler and await the request so Lambda observes success or failure correctly.
Cannot use import statement outside a module
The sample is an ES module. Save it as index.mjs and keep the handler set to index.handler. If you instead rename the file to index.js, either retain "type": "module" in package.json or rewrite the imports using CommonJS require syntax.
Do not mix module systems casually. The deployment package needs to match the file name, package.json settings, and handler value that Lambda loads.
Cannot find package 'axios'
This error means Lambda did not receive the installed dependency. Confirm that node_modules/axios exists before creating the ZIP and that the ZIP contains node_modules at its root alongside index.mjs.
A frequent packaging error is zipping the parent directory rather than its contents. In that case Lambda may receive volanea-lambda-email/index.mjs instead of index.mjs, and module resolution or handler loading fails. Run the zip -r function.zip index.mjs package.json node_modules command from inside the project directory.
Timeout, DNS, or connection errors
The code sets a 10-second client timeout so an unreachable upstream does not consume the entire Lambda execution window. If you see ECONNABORTED, ENOTFOUND, or connection errors, check Lambda’s timeout setting, VPC routing, NAT configuration, security controls, and outbound DNS access.
If the function is not attached to a VPC, outbound HTTPS is typically straightforward. If it is attached to private subnets, verify that its networking design permits outbound calls to public HTTPS services. Do not “fix” a network error by expanding inbound permissions; this integration needs outbound connectivity.
Duplicate sends after retries
A timeout does not prove the provider did not receive the request. If your trigger retries automatically, model a send as a business operation with a stable identifier, persist its state, and decide which failures are safe to retry. A queue plus an outbound-email record is often safer than retrying directly from an HTTP request handler.
For especially sensitive mail such as receipts or one-time login links, generate one message intent per business event and reuse the same intent during recovery. This lets your system reason about duplicate execution even when infrastructure delivers an event more than once.
Logging, monitoring, and operational safety
The sample logs HTTP status and provider response data when available. That is useful for an initial integration, but production logging needs a deliberate privacy policy. Do not log message bodies, authentication headers, reset URLs, recipient addresses, or personally identifiable information unless the data is necessary, access-controlled, and retained according to your policy.
Log a correlation ID instead. For example, include an internal emailJobId, orderId, or requestId in your own logs and database. When an event fails, your support and engineering teams can trace the message flow without searching for a customer’s email address or copying secrets into tickets.
Set a Lambda timeout that is longer than the HTTP client timeout. With a 10-second Axios timeout, a Lambda timeout of several seconds beyond that gives the handler time to record an error and return a clear failure. Avoid a timeout so short that AWS ends the function while the request is still in flight.
Monitor at least these signals:
- Lambda errors and duration.
- Lambda throttles and concurrent-execution limits.
- HTTP response-status distribution from Volanea requests.
- Queue age and dead-letter activity if sends are asynchronous.
- Delivery, bounce, complaint, and unsubscribe events after acceptance.
The send endpoint can be the beginning of your observability chain, not the end. A system that only knows “the Lambda did not crash” cannot answer the customer-support question that matters: what happened to this particular message?
Next steps: templates and webhooks
Once the direct send works, move repeated transactional layouts into reusable templates. A template centralizes the subject and content structure while your application supplies recipient-specific variables, reducing duplicated HTML across Lambda functions. Volanea’s template API stores reusable content addressed by a templateId, allowing sends to reference a template rather than carry the full markup every time. (volanea.com)
Add webhooks next. A webhook is an HTTPS endpoint in your application that receives event notifications after the original send request, such as later delivery or failure-related events. Verify webhook signatures if your webhook configuration provides them, respond quickly with a successful status, deduplicate repeated events, and hand longer processing to a queue.
For endpoint details, message fields, templates, and setup guidance, consult the Volanea API reference and setup documentation. Keep template changes and webhook handlers under version control or a documented release process so an email-content change is as reviewable as an application-code change.
FAQ
Can AWS Lambda send email with Volanea without SMTP?
Yes. This guide uses Volanea’s HTTPS REST send endpoint rather than SMTP. The Lambda function sends a JSON POST request to /v1/send, so no persistent SMTP connection is required. (volanea.com)
Do I need to install the AWS SDK for this integration?
No. The sample only needs axios because it calls Volanea over HTTPS. You may need AWS SDK packages separately if your application retrieves API keys from AWS Secrets Manager, reads queue messages manually, or interacts with other AWS services in code.
Where should I store the Volanea API key?
For an initial Lambda setup, store it in the VOLANEA_API_KEY environment variable. For stronger production secret-management practices, AWS recommends Secrets Manager for sensitive credentials such as API keys and authorization tokens. (docs.aws.amazon.com)
Should I send email directly from an API Gateway Lambda?
You can, but for important transactional flows an asynchronous queue or durable outbox pattern is often safer. It separates the user-facing request from transient provider or network failures and gives you a controlled retry path.
Why did the API request succeed but the message is not in the inbox?
API acceptance is not final delivery. Check the sender-domain configuration, recipient address, junk folder, suppression state, and delivery-event webhooks. Use those events to determine whether the message was delivered, bounced, complained about, or otherwise failed after submission.