Send email with Cloudflare Workers by calling Volanea’s REST API from your Worker with the platform-native fetch() function. This guide creates a Worker, stores the Volanea API key as a secret, and sends one transactional email without relying on a Node-only SMTP library or an unverified provider SDK.
Cloudflare Workers are a strong fit for transactional email triggers that happen at the edge: contact-form notifications, account alerts, verification messages, receipt requests, and application events. A Worker can make an outbound HTTPS request directly, so the most portable integration is a JSON request to Volanea’s POST /v1/send endpoint.
This guide uses TypeScript and Wrangler. The finished Worker accepts an HTTP request, creates a transactional email payload, sends it through Volanea, and returns the provider response as JSON.
What you will build
By the end of this guide, you will have a Cloudflare Worker that:
- Reads a Volanea API key from the
VOLANEA_API_KEYenvironment secret. - Sends one HTML and plain-text transactional email through
https://api.volanea.com/v1/send. - Uses standard Web Platform APIs available in Workers:
fetch,Request,Response, andJSON.stringify. - Handles provider failures without exposing your API key to the caller.
- Can be tested locally with Wrangler and deployed with the same project configuration.
The example deliberately uses the REST API rather than SMTP. Cloudflare Workers are designed around the Fetch API and do not provide the long-lived TCP socket model most SMTP client packages expect. An HTTPS email API avoids that mismatch and gives your Worker an ordinary request-and-response workflow.
Before sending production traffic, make sure the from address belongs to a verified sending domain in Volanea. A verified domain is important because recipient providers use authentication signals, including SPF and DKIM, when evaluating mail. The send endpoint can accept one recipient or multiple recipients, but this first example sends only one message to one address.
Prerequisites
You need the following before starting:
- A Cloudflare account with permission to create and deploy Workers.
- Node.js installed locally. Wrangler requires Node.js 16.17.0 or newer.
- A Volanea account and a secret API key beginning with
sk_orsk_test_. - A verified Volanea sending domain and an email address on that domain, such as
notifications@example.com. - A recipient address you control for testing.
Use a test API key while validating the integration if your Volanea account provides one. Once the Worker has the correct sender, payload shape, secret binding, and response handling, replace the test key with the production key only when you are ready to deliver mail.
Do not put your Volanea API key in source code, a browser application, a public Git repository, or wrangler.jsonc under vars. A Worker secret is available to Worker code like an environment variable, but its value is encrypted and is not displayed after it has been set.
Create a Cloudflare Worker project
Create a new Worker-only project using Cloudflare’s project generator:
npm create cloudflare@latest -- volanea-worker
During setup, choose these options:
- What would you like to start with? Select Hello World example.
- Which template would you like to use? Select Worker only.
- Which language do you want to use? Select TypeScript.
- Do you want to deploy your application? Select No for now.
Then enter the project directory:
cd volanea-worker
The generated project includes Wrangler, the Cloudflare Worker command-line tool. If you are adding this integration to an existing Worker project that does not already include Wrangler, install it as a development dependency:
npm i -D wrangler@latest
There is no Volanea-specific Cloudflare Workers SDK required for this guide. The dependency you install is Wrangler, and the sending request uses the Workers-native Fetch API. This keeps the integration compatible with the Workers runtime and avoids depending on Node modules that may require unavailable TCP, filesystem, or process APIs.
Configure the Worker
Open wrangler.jsonc. A minimal Worker configuration typically identifies the Worker name, source entry point, and compatibility date. Keep the generated compatibility date if your project generator already created one.
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "volanea-worker",
"main": "src/index.ts",
"compatibility_date": "2026-04-01"
}
The date in the example is only a compatibility-date value for the Worker runtime; use the date created by your project or a date appropriate for your deployment process. Do not add VOLANEA_API_KEY to this file. Configuration variables in vars are appropriate for non-sensitive values, but API keys belong in secrets.
For a production Worker, it is useful to keep non-secret configuration separate from credentials. For example, you may later add a non-secret sender address such as MAIL_FROM, a feature flag for mail routes, or a service URL. However, keeping the sender in code initially makes the copy-pasteable example easier to inspect. Move it to a regular environment variable when your application needs different sender addresses across staging and production.
Add the Volanea API key as a secret
Set the API key on your deployed Worker with Wrangler:
npx wrangler secret put VOLANEA_API_KEY
Wrangler prompts you for the secret value. Paste your Volanea secret key when prompted, then press Enter. Do not include quotation marks around the key unless quotation marks are part of the key itself.
For local development, create a .dev.vars file in the project root:
VOLANEA_API_KEY=sk_test_replace_with_your_test_key
Add .dev.vars to .gitignore. Cloudflare recommends using either .dev.vars or .env for local Worker secrets, not both. The local file lets wrangler dev provide VOLANEA_API_KEY through the Worker’s env parameter without committing the credential.
A secret name is case-sensitive. The code below reads env.VOLANEA_API_KEY, so the secret must be named exactly VOLANEA_API_KEY. A secret named VOLANEA_APIKEY, VOLANEA_KEY, or volanea_api_key will not be found by this Worker.
Send email with Cloudflare Workers
Replace the contents of src/index.ts with the following complete example.
interface Env {
VOLANEA_API_KEY: string;
}
const VOLANEA_SEND_URL = "https://api.volanea.com/v1/send";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== "POST") {
return new Response("Send a POST request to trigger the email.", {
status: 405,
headers: {
Allow: "POST",
"Content-Type": "text/plain; charset=utf-8",
},
});
}
if (!env.VOLANEA_API_KEY) {
return Response.json(
{ error: "VOLANEA_API_KEY is not configured." },
{ status: 500 },
);
}
const emailPayload = {
from: "Volanea Demo <notifications@your-verified-domain.com>",
to: "you@example.com",
subject: "Your Cloudflare Worker sent this email",
html: `
<h1>Cloudflare Worker email sent</h1>
<p>This transactional email was sent through Volanea's REST API.</p>
`,
text: "Cloudflare Worker email sent. This transactional email was sent through Volanea's REST API.",
};
try {
const volaneaResponse = await fetch(VOLANEA_SEND_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${env.VOLANEA_API_KEY}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(emailPayload),
});
const responseText = await volaneaResponse.text();
let responseBody: unknown = responseText;
try {
responseBody = JSON.parse(responseText);
} catch {
// Preserve a non-JSON upstream response as text for troubleshooting.
}
if (!volaneaResponse.ok) {
console.error("Volanea send failed", {
status: volaneaResponse.status,
responseBody,
});
return Response.json(
{
error: "Volanea rejected the email request.",
upstreamStatus: volaneaResponse.status,
details: responseBody,
},
{ status: 502 },
);
}
return Response.json(
{
ok: true,
message: "Email accepted by Volanea.",
result: responseBody,
},
{ status: 200 },
);
} catch (error) {
console.error("Unable to reach Volanea", error);
return Response.json(
{
error: "The Worker could not reach the Volanea API.",
},
{ status: 502 },
);
}
},
};
Replace both placeholder addresses before testing:
- Replace
notifications@your-verified-domain.comwith an address on a domain verified in Volanea. - Replace
you@example.comwith a recipient address you can access.
The from field includes a display name and mailbox in standard address form. The to field is a single recipient for this quickstart. The subject, html, and text fields provide the transactional message content. Including both html and text gives email clients a plain-text alternative when HTML is unavailable or disabled.
The API key is read inside the Worker from env.VOLANEA_API_KEY. That is important: do not use process.env.VOLANEA_API_KEY in this example. Cloudflare Workers pass bindings and secrets through the env parameter, and this approach works without enabling Node.js compatibility.
How the sending request works
The most important part of the Worker is the outbound request:
const volaneaResponse = await fetch("https://api.volanea.com/v1/send", {
method: "POST",
headers: {
Authorization: `Bearer ${env.VOLANEA_API_KEY}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(emailPayload),
});
This request has four required implementation details:
- Use
POST. Sending creates a new email-send request, so the Worker calls the Volanea send endpoint withPOST. - Use bearer authentication. The API key is sent in the
Authorizationheader asBearer <key>. - Set JSON content type.
Content-Type: application/jsontells the API to parse the request body as JSON. - Serialize the body.
fetch()does not automatically convert a JavaScript object into JSON.JSON.stringify(emailPayload)is required.
The Worker awaits the Volanea response before returning. That means a caller receives a success response only after the upstream API has accepted the HTTP request. It does not mean that the message has already appeared in an inbox. Acceptance, delivery to a recipient server, bounce processing, and user engagement are separate lifecycle events.
The example returns the provider response under result. That is convenient while testing, but be cautious about passing upstream responses directly to a public caller in a production route. Depending on your application, you may prefer to log the provider result internally and return only a message ID or a generic acceptance response.
Test the Worker locally
Start the Worker development server:
npm run dev
Wrangler prints a local URL, commonly on port 8787. In a second terminal, trigger the Worker with a POST request:
curl -X POST http://localhost:8787
A successful request returns JSON similar to this shape:
{
"ok": true,
"message": "Email accepted by Volanea.",
"result": {}
}
The exact result fields depend on the Volanea API response. Store any message identifier returned by the send API if your application needs to correlate application events with later delivery events.
If the request returns a 500 error saying the API key is not configured, verify that .dev.vars exists in the same directory as wrangler.jsonc, contains the exact secret name, and has no accidental spaces around the variable name. Restart npm run dev after changing local secret files.
If the Worker returns a 502 with an upstream status in its JSON body, the Worker reached Volanea but Volanea rejected the payload or credentials. Read the details value and compare the sender, authentication header, and request payload against the email API reference and setup guides.
Deploy the Worker
When local testing succeeds, deploy the Worker:
npm run deploy
If your starter project does not include a deploy script, use Wrangler directly:
npx wrangler deploy
Set the production secret before or after deployment with:
npx wrangler secret put VOLANEA_API_KEY
Setting a Worker secret creates a new Worker version and deploys it. Confirm that the secret has been applied to the same Worker and environment you are calling. If you use Wrangler environments such as staging and production, configure the key for each environment explicitly rather than assuming a secret from one environment is inherited by another.
After deployment, send a request to your Worker URL:
curl -X POST https://your-worker.your-subdomain.workers.dev
For a real application, do not leave an unrestricted public route that sends mail every time it receives a POST. Put the send operation behind application authentication, validate incoming data, rate-limit the triggering route, and make sure an attacker cannot choose arbitrary recipient addresses or arbitrary HTML content.
Adapt the example for application data
The quickstart intentionally hard-codes the recipient and email copy. In an application, accept structured data only after validating it. For example, a signup flow might pass a server-generated recipient address and a one-time verification URL. A contact form might always send to a fixed support address rather than trusting a user-supplied to value.
A safe pattern is:
- Authenticate the caller before processing the request.
- Parse the request body with
await request.json(). - Validate required strings, lengths, and allowed values.
- Generate the email content from trusted server-side data.
- Keep the sender address fixed or select it from a small allowlist.
- Send the API request with
awaitand handle non-2xx responses. - Store the returned send identifier with the application event when available.
Avoid turning your Worker into an open email relay. A route that accepts any destination address, subject, and HTML from unauthenticated callers can be abused for spam, phishing, and cost amplification. Even authenticated users should be subject to authorization and rate limits appropriate to the feature.
Parsing a JSON request safely
If you later accept a recipient from your application request, handle invalid JSON before creating an email payload:
let input: { email?: unknown };
try {
input = await request.json();
} catch {
return Response.json({ error: "Request body must be valid JSON." }, { status: 400 });
}
if (typeof input.email !== "string" || input.email.length === 0) {
return Response.json({ error: "A recipient email is required." }, { status: 400 });
}
This checks only that a string is present. Production validation should also enforce your product’s rules, such as maximum length, normalization, account ownership, or a verified-user requirement. Do not rely on a client-provided address alone to decide who receives sensitive account messages.
Avoid duplicate transactional sends
Workers can be retried by clients, gateways, queues, or your own application code. A timeout does not prove that Volanea did not receive the original request; it may mean the response was lost after the upstream accepted it. For important messages such as receipts and password resets, design the surrounding workflow so retries do not accidentally create duplicates.
A practical approach is to create an application-level event ID before calling the send API, persist it in your database, and mark it as sent only after a successful response. If a retry finds the event already sent, return the existing result instead of issuing another email request. Where your sending workflow uses an API-supported idempotency mechanism, use a stable key derived from that application event rather than generating a new random value on every retry.
Common errors
Authentication failures: 401 or 403 responses
A 401 or 403 response usually means Volanea could not authenticate or authorize the request. Check these items in order:
- Confirm
VOLANEA_API_KEYis set as a Worker secret, not only in a local file. - Confirm the code reads
env.VOLANEA_API_KEYwith the exact same capitalization. - Confirm the header is
Authorization: Bearer <API_KEY>. - Confirm you did not include the placeholder value from this guide.
- Confirm the key is active and belongs to the intended Volanea account or environment.
- Confirm that a test key and production key are not being mixed accidentally.
Do not log the value of env.VOLANEA_API_KEY while debugging. Log whether the value exists, the returned status code, and a redacted key prefix only if your security policy permits it.
Wrong or missing Content-Type
If the API says the payload is invalid, unsupported, or unreadable, verify this header is present:
"Content-Type": "application/json"
Also verify that the body is a string created with JSON.stringify(). This is incorrect:
body: emailPayload
This is correct:
body: JSON.stringify(emailPayload)
An object passed directly as body is not the JSON request Volanea expects. The content-type and serialized body must agree.
Missing await on fetch
Workers use asynchronous JavaScript. If you call fetch() without await, your code receives a Promise, not an HTTP response. That causes errors when you try to read .ok, .status, or .text().
Incorrect:
const response = fetch(VOLANEA_SEND_URL, options);
if (!response.ok) {
// response is a Promise, not a Response
}
Correct:
const response = await fetch(VOLANEA_SEND_URL, options);
if (!response.ok) {
// response is an HTTP Response
}
Likewise, use await volaneaResponse.text() or await volaneaResponse.json() when reading the upstream body. Response-body methods are asynchronous because the body may still be streaming.
Sender-domain verification errors
If Volanea rejects the from address, make sure the domain portion of the sender is verified in Volanea and that the exact sender address meets your account’s sending rules. Do not use arbitrary consumer-mailbox addresses as your production sender. Use an address on a domain your organization controls, such as notifications@yourdomain.com.
Sender verification failures are different from recipient delivery failures. A sender verification issue prevents the send request from being accepted. A delivery event occurs later, after an accepted message is processed and handed off to recipient infrastructure.
HTML content errors or malformed markup
The API accepts the HTML string as message content, but the Worker is responsible for forming valid JSON and for safely generating HTML. Use template literals for static markup and escape or encode untrusted values before interpolating them into an HTML email.
For example, never directly interpolate a user-controlled name into HTML without escaping it:
html: `<p>Welcome, ${untrustedName}</p>`
Treat email HTML as an output context. Escape dynamic content, avoid accepting arbitrary HTML from users, and keep transactional copy generated by trusted application code.
Local secret is undefined
When wrangler dev cannot find VOLANEA_API_KEY, check that you used .dev.vars, not a file named .dev.vars.txt. Verify it is in the project root, beside wrangler.jsonc. Use one local secret-file convention at a time: .dev.vars or .env.
Restart the local development server after changing secret files. A running local Worker may not reload a newly added secret binding until it is restarted.
Returning upstream error details to end users
The quickstart returns upstream details because they help during integration. In a public production endpoint, these details may reveal information you would rather keep in logs. Return a generic error to the caller, log the provider status internally, and attach a request ID so you can investigate the event without exposing provider responses.
Production considerations
A working send request is the beginning of an email integration, not the end. Production transactional email needs an operational design around authentication, sending permissions, retries, observability, recipient preferences, and delivery outcomes.
Keep API keys scoped and rotatable
Use separate keys for development, staging, and production when your account configuration supports them. Store each key as a secret in the relevant Worker environment. Rotate keys on a schedule and immediately after a suspected exposure. During rotation, deploy the new secret first, validate sends, and revoke the old credential only after every deployed environment is using the replacement.
Separate user-facing requests from slow work
The sample waits for Volanea’s API response before completing the HTTP request. That is appropriate for a simple contact form or a user action where immediate acceptance feedback matters. For higher-volume jobs, consider placing email work behind a queue or an application job system so the user-facing request does not have to manage retries and send throughput directly.
The right architecture depends on the message type. Password-reset and verification messages are usually time-sensitive. Large sets of account notices, exports, and scheduled notifications can often be queued and processed with bounded concurrency. In both cases, persist enough application context to understand why the email was requested and whether it was accepted.
Log safely
Useful logs include the Worker request ID, route name, application event ID, recipient domain, provider status code, and returned message identifier. Avoid logging full recipient addresses, email body content, verification tokens, authorization headers, or API keys unless you have a specific privacy-reviewed reason to do so.
Logging recipient domains rather than complete recipient addresses can often reveal deliverability patterns while reducing exposure of personal data. For example, a rise in failures at one recipient domain may be operationally useful without recording every mailbox.
Validate email addresses before expensive workflows
Syntax validation alone cannot guarantee a mailbox is deliverable or appropriate to contact. Validate user input for your application’s requirements, confirm ownership when necessary, and honor suppression and unsubscribe rules for message categories where those apply. Before starting an expensive onboarding, notification, or campaign workflow, you can use an address verification tool as an additional input to your decision process.
Next steps
Once this first send works, expand the integration in two directions.
First, use templates for reusable transactional layouts. Instead of assembling every subject line and HTML document in Worker code, store a reviewed template and send the data needed to personalize it. This reduces duplicate markup across routes, makes email changes easier to review, and helps maintain consistent plain-text and HTML alternatives.
Second, add webhooks for email lifecycle events. A send API response confirms that Volanea accepted the request; webhooks let your application react to later outcomes such as delivery, bounces, complaints, or other provider events. Verify webhook signatures, make handlers idempotent, and return success quickly before processing any longer-running follow-up work.
Also consider adding a queue-backed send workflow, application-level idempotency, structured logs, and separate environment secrets before sending business-critical email at scale.
FAQ
Can Cloudflare Workers send email through Volanea?
Yes. A Cloudflare Worker can call Volanea’s HTTPS REST API with the built-in fetch() function. Store the Volanea API key as a Worker secret and send a JSON POST request to https://api.volanea.com/v1/send.
Do I need a Volanea SDK for Cloudflare Workers?
No. This guide uses the standard Fetch API available in Workers, so no Volanea-specific SDK is required. This is often the most compatible approach for edge runtimes because it uses standard HTTPS requests rather than Node-specific transport dependencies.
Why should I use the env parameter for the API key?
Cloudflare Workers expose secrets and bindings through the env parameter passed to the Worker handler. Reading env.VOLANEA_API_KEY keeps the credential out of source code and works without assuming Node.js environment-variable behavior.
Why did Volanea accept my request but the recipient did not receive an email?
An accepted send request means Volanea received the message request. Inbox appearance can still depend on later delivery processing, recipient-server acceptance, spam filtering, mailbox rules, bounces, and suppression status. Use provider events and webhooks to observe the rest of the email lifecycle.
Can I use SMTP from a Cloudflare Worker?
Use Volanea’s REST API for this Worker integration. SMTP clients generally rely on socket connections that do not match the standard Cloudflare Workers Fetch API model. An HTTPS REST request is the direct, runtime-compatible way to send transactional email from a Worker.