Send email with Deno Deploy by calling Volanea’s REST API from a Deno HTTP handler. This guide gives you a complete TypeScript example that loads configuration from environment variables, sends one transactional email, reports useful errors, and is ready to deploy.

What you will build

You will create a small Deno Deploy application with one POST /send-test-email endpoint. When that endpoint receives a request, it sends one transactional email through Volanea.

The sample deliberately uses the REST API rather than an unverified framework-specific client. Deno Deploy supports the standard Fetch API, so you can call Volanea directly without adding a Node-only mail transport, managing SMTP sockets, or depending on a provider-specific SDK method.

The application reads these environment variables:

  • VOLANEA_API_KEY: Your Volanea secret API key.
  • EMAIL_FROM: A sender address on a domain that is ready to send with Volanea.
  • TEST_RECIPIENT_EMAIL: The address that receives the sample transactional email.

The sender and recipient are stored as environment variables rather than accepted from a public request body. That makes the endpoint safe for a basic integration test: an arbitrary caller cannot turn it into an open email relay by choosing recipients or sender identities.

For a production application, invoke the same sending logic from an authenticated route or from your server-side business event handler. Examples include an order-confirmed handler, a password-reset workflow, an invitation endpoint, or a billing notification job.

Before you start

You need the following before sending a live email:

  1. A Volanea account and a secret API key.
  2. A sender domain configured for sending in Volanea.
  3. A Deno installation for local development.
  4. A Deno Deploy application for the deployed version.
  5. An email address you control for testing.

Do not place the Volanea API key in browser JavaScript, a public Git repository, a client-side form, or a URL query string. An API key authorizes server-side sending. Anyone who obtains it may be able to send mail through your account until you rotate or revoke the key.

Volanea secret keys use the sk_ or sk_test_ style shown in the API documentation. Use a test key when your account supports test mode, and use a real recipient address that you can inspect when validating a production-domain setup.

The REST endpoint in this guide is:

POST https://api.volanea.com/v1/send

The request sends JSON and uses a Bearer token in the Authorization header. The minimal message fields are a sender, recipient, subject, and email content.

Install the Deno dependency

This integration uses @std/dotenv only for local development convenience. It lets the same project load values from a local .env file while using Deno Deploy environment variables after deployment.

Run this command from the root of your Deno project:

deno install jsr:@std/dotenv

Deno records the dependency in your project configuration and caches it. The email request itself uses Deno’s built-in fetch, so there is no unverified Volanea SDK to install.

Create a new project if you do not already have one:

mkdir volanea-deno-deploy
cd volanea-deno-deploy
deno init
deno install jsr:@std/dotenv

Your project will contain a Deno configuration file and a TypeScript entrypoint. Replace the generated main.ts with the example below.

Create a local environment file

Create a .env file for local development:

VOLANEA_API_KEY=sk_test_replace_with_your_key
EMAIL_FROM=Example App <hello@your-verified-domain.example>
TEST_RECIPIENT_EMAIL=you@example.com

Replace every placeholder before running the application. EMAIL_FROM must be an address you are authorized to use with Volanea. A display name followed by an email address in angle brackets is useful for recognizable transactional messages, but the underlying domain must still be configured correctly.

Add .env to .gitignore before committing code:

.env

Do not commit a real API key, even in a private repository. Repositories can be cloned, backups can persist, and build logs can accidentally expose configuration values.

Complete Deno Deploy email example

Save the following as main.ts. It is a complete Deno HTTP application. It loads .env only during local execution, reads deployed configuration through Deno.env.get, calls Volanea with JSON, and returns a safe response to the caller.

import { load } from "jsr:@std/dotenv";

// Deno Deploy provides environment variables at runtime. For local development,
// load values from .env into Deno.env when the deployment identifier is absent.
if (!Deno.env.get("DENO_DEPLOYMENT_ID")) {
  await load({ export: true });
}

const apiKey = Deno.env.get("VOLANEA_API_KEY");
const from = Deno.env.get("EMAIL_FROM");
const recipient = Deno.env.get("TEST_RECIPIENT_EMAIL");

if (!apiKey || !from || !recipient) {
  throw new Error(
    "Missing VOLANEA_API_KEY, EMAIL_FROM, or TEST_RECIPIENT_EMAIL environment variable.",
  );
}

Deno.serve(async (request) => {
  const url = new URL(request.url);

  if (request.method !== "POST" || url.pathname !== "/send-test-email") {
    return new Response("Not found", { status: 404 });
  }

  const emailResponse = await fetch("https://api.volanea.com/v1/send", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      from,
      to: recipient,
      subject: "Your Volanea + Deno Deploy test email",
      html: `
        <h1>Email sent from Deno Deploy</h1>
        <p>This transactional email was sent through the Volanea REST API.</p>
        <p>Sent at: ${new Date().toISOString()}</p>
      `,
      text: `Email sent from Deno Deploy. Sent at: ${new Date().toISOString()}`,
    }),
  });

  const responseText = await emailResponse.text();

  if (!emailResponse.ok) {
    console.error("Volanea send failed", {
      status: emailResponse.status,
      response: responseText,
    });

    return Response.json(
      {
        error: "Volanea rejected the email request.",
        status: emailResponse.status,
      },
      { status: 502 },
    );
  }

  console.log("Volanea accepted the email request", {
    status: emailResponse.status,
    response: responseText,
  });

  return Response.json({
    ok: true,
    message: "Volanea accepted the transactional email request.",
  });
});

This example does not parse or return Volanea’s full response body to the HTTP caller. That is intentional. Provider responses can include IDs and operational details that are useful in server logs, but your public endpoint should expose only the data your application needs.

The console.log and console.error calls retain the provider response text for deployment logs. Avoid logging the API key, the complete Authorization header, or user-provided email content that may contain sensitive information.

How the request works

The important part of the integration is the call to fetch:

const emailResponse = await fetch("https://api.volanea.com/v1/send", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from,
    to: recipient,
    subject: "Your Volanea + Deno Deploy test email",
    html: "<p>Hello from Deno Deploy.</p>",
    text: "Hello from Deno Deploy.",
  }),
});

Authentication

The Authorization header must contain the exact API key value from VOLANEA_API_KEY prefixed by Bearer .

Do not use the key as the recipient, do not put it into the JSON payload, and do not use an environment-variable name in place of its value. This is correct:

"Authorization": `Bearer ${apiKey}`

This is not correct:

"Authorization": "Bearer VOLANEA_API_KEY"

The second form sends the literal text VOLANEA_API_KEY, which causes authentication to fail.

JSON content type

The Content-Type header tells Volanea how to interpret the request body. Because JSON.stringify creates JSON, use:

"Content-Type": "application/json"

Do not send form-encoded content, multipart content, or raw HTML as the entire request body for this endpoint. The HTML belongs inside the html field of the JSON message object.

HTML and plain-text content

The sample includes both html and text. HTML provides the formatted version of the email. The text version is a readable alternative for mail clients that prefer plain text, for accessibility tools, and for recipients whose client disables HTML rendering.

Keep the two versions semantically aligned. If the HTML says an order has shipped, the text part should communicate the same order status, tracking link, and essential details. Do not put required information solely inside an image or HTML-only button.

Sender address

The from field identifies the sender. It must use a domain that is ready to send through Volanea. A request can be technically valid JSON and still be rejected if its sender is not authorized.

Use a stable sender address for a message category when possible. For example, send receipts from receipts@, security notifications from security@, and product notices from updates@. This gives recipients predictable context and makes mailbox rules easier to configure.

Run the app locally

Start the application locally with network, environment, and file-read permissions:

deno run --allow-net --allow-env --allow-read main.ts

The --allow-net permission permits the outgoing request to Volanea and the local HTTP server. The --allow-env permission permits access to the environment variables. The --allow-read permission permits @std/dotenv to read the local .env file.

In a separate terminal, trigger the transactional message:

curl -X POST http://localhost:8000/send-test-email

A successful request returns JSON similar to this:

{
  "ok": true,
  "message": "Volanea accepted the transactional email request."
}

Acceptance by the API means Volanea accepted the request for processing. It is not the same as proof that a recipient opened the message or that a destination mailbox displayed it. Delivery and engagement are separate stages, which is why production integrations should record send results and process email events.

Check the recipient inbox and spam folder. If the call succeeds but the email is not visible, confirm the sender domain configuration, inspect the message status in your Volanea account, and verify that the recipient address is correct.

Deploy to Deno Deploy

Commit the project without the .env file. Your deployment should receive its secret values from Deno Deploy runtime environment variables rather than from source control.

Create a Deno Deploy application from your repository or use your existing application. Configure the project’s dependency installation command as:

deno install

Deno Deploy can install Deno dependencies during the build. No separate TypeScript compilation command is required for this small application because Deno runs TypeScript directly.

Add the following runtime variables to the deployed application configuration:

VOLANEA_API_KEY
EMAIL_FROM
TEST_RECIPIENT_EMAIL

Set VOLANEA_API_KEY to the real key for the environment you are deploying. Set EMAIL_FROM to the approved sender identity. Set TEST_RECIPIENT_EMAIL to an inbox your team controls.

After deployment, send an HTTP POST request to:

https://your-deployment-url/send-test-email

The route is intentionally narrow, but it still sends email whenever it is called. Do not leave a test-send endpoint publicly callable in a production application. Protect it with your application’s authentication and authorization layer, restrict it to internal traffic, remove it after validation, or replace it with a business-event route that cannot be invoked anonymously.

Use different values by environment

Keep development, staging, and production configuration separate. A practical arrangement is:

  • Development: test key, test sender, team-owned inbox.
  • Staging: staging key or test mode, staging domain, controlled QA inbox.
  • Production: production key, production sender, real transactional recipients.

This separation prevents a local test from accidentally notifying a customer. It also reduces the chance that a staging application damages the sending reputation or analytics of a production domain.

Adapt the sample for transactional events

The test endpoint uses a fixed recipient because its job is to validate credentials and sender configuration. Real transactional email normally uses data from a trusted server-side event.

For example, an order-confirmation handler could construct the payload from an order record that has already been validated and stored:

const message = {
  from: "Example Store <orders@your-verified-domain.example>",
  to: customer.email,
  subject: `Order ${order.number} confirmed`,
  html: `<p>Thanks for your order, ${customer.firstName}.</p>`,
  text: `Thanks for your order, ${customer.firstName}.`,
};

Do not allow a browser to submit unrestricted from, to, subject, or HTML fields to an email API route. A frontend user may be authenticated but still should not automatically have permission to send arbitrary mail. Apply authorization rules, validate the purpose of the message, and derive sensitive values from records stored on the server.

For password resets, do not generate a reset token in email HTML without first storing a hashed or otherwise verifiable token server-side. For receipts, retrieve totals and item names from your payment or order database rather than trusting values supplied by the browser. For invitations, ensure the inviter has permission to invite the target user.

These controls are application responsibilities. The email API delivers a message request; your code must decide when that request is valid.

Common errors

401 or 403 authentication failures

An authentication error usually means the API key is missing, malformed, revoked, from the wrong environment, or sent in the wrong header format.

Check all of the following:

  • VOLANEA_API_KEY exists in the Deno Deploy runtime environment.
  • The value does not include surrounding quotes copied from a dashboard or shell command.
  • The key is used as Authorization: Bearer <key>.
  • You did not deploy a test key where a live key is required, or vice versa.
  • You redeployed after changing runtime configuration when your deployment workflow requires it.

Never debug this by printing the full key. Log whether the variable exists and, if necessary, rotate the key after correcting configuration.

400 request validation errors

A 400 response generally means Volanea received the request but rejected one or more fields. Common causes include a missing sender, recipient, subject, or message body; malformed JSON; an invalid email address; or a sender that is not permitted.

The sample logs Volanea’s response text when a request fails. Inspect deployment logs for the provider’s validation detail, then correct the payload rather than retrying unchanged input repeatedly.

Wrong Content-Type

If you omit Content-Type: application/json, send text/plain, or pass an object directly as body, the API may not parse the request as JSON.

Use both of these pieces together:

headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),

Do not write this:

body: payload

The Fetch API expects a string, bytes, stream, form data, or other supported body type. A plain JavaScript object is not automatically serialized as JSON.

Forgetting await

fetch is asynchronous. If you do not await it, your function continues before the response is available, and error handling may not run as intended.

Correct:

const response = await fetch(url, options);

Incorrect:

const response = fetch(url, options);
if (!response.ok) {
  // response is a Promise, not an HTTP Response
}

The same rule applies to response.text() and response.json(). Await the method before using the decoded body.

Using response.json() when the error body is not JSON

Error bodies are not always JSON. Calling await response.json() can itself throw and hide the original HTTP failure. The sample uses await response.text() so it can log whatever response representation is returned.

If your application knows the endpoint always responds with JSON, parse it after checking response.ok or wrap JSON parsing in a try/catch block.

Missing Deno permissions during local development

Local Deno execution uses explicit permissions. If you receive a permission error, rerun the command with the permissions the application needs:

deno run --allow-net --allow-env --allow-read main.ts

Do not solve permission errors by using unrestricted permissions for every script as a default. Grant only the permissions required by the project, especially when running unfamiliar code.

Sender-domain failures

A sender address can look valid while still being unavailable for API sending. Confirm that EMAIL_FROM uses the domain you configured in Volanea, that the value has no accidental whitespace, and that you are not trying to send from a consumer mailbox address that is unrelated to your approved sending domain.

Timeouts and duplicate sends

A network timeout does not always mean no message was accepted. The request may have reached Volanea, but the connection could have closed before your application received the response. Blindly retrying a transactional send can create duplicates.

For production-critical messages, create an application-level send record before sending, associate it with the event that caused the email, and make retry decisions from that record. If the API reference supports an idempotency mechanism for your use case, follow its documented format and reuse the same key for retries of the same logical message.

Make the integration production-ready

A successful test proves the basic path works. Production email needs more than a successful fetch call.

First, move email sending behind a service function. The HTTP route, queue worker, cron task, or application event handler should call that function rather than duplicating headers and payload logic across the codebase. Centralizing the integration makes sender changes, error handling, observability, and future template adoption easier.

Second, separate message creation from message delivery attempts. For example, store a password_reset_requested or order_confirmed event in your database, then have a worker send the corresponding email. If the provider request fails temporarily, your worker can retry according to your policy without making a user repeat their action.

Third, validate recipient data before it reaches the sending code. Basic syntax validation is helpful, but it does not prove a mailbox can receive mail. For user-facing signup and lead-capture flows, use an email address verification tool before relying on an address for important transactional communication.

Fourth, treat email content as application output. Escape user-generated data before placing it in HTML, keep reset links short-lived, avoid secrets in subject lines, and make security messages clear enough that recipients can identify suspicious activity.

Finally, monitor the full lifecycle. Sending, provider acceptance, delivery, bounce processing, complaints, and unsubscribes all provide different signals. A system that only logs a 200-range API response can miss address-quality problems and deliverability issues.

Next steps

After the basic send works, replace hard-coded markup with reusable templates for message types such as receipts, invitations, password resets, and account alerts. Templates make it easier to keep branding and legal copy consistent while letting your code provide event-specific values.

Also add webhook handling. Webhooks let your application receive message-event notifications, such as delivery outcomes and bounce-related events, so you can update internal records, suppress bad addresses, and investigate failures without polling manually.

For endpoint details, message fields, templates, and event-related API guidance, use the Volanea API reference and setup guides. When you add webhooks, verify signatures according to the documented webhook security procedure before trusting an incoming event.

FAQ

Can I send email with Deno Deploy without an SMTP library?

Yes. This guide uses Deno’s built-in Fetch API to make an HTTPS request to Volanea’s REST endpoint. You do not need an SMTP client library for this REST-based implementation.

Why does the example include @std/dotenv?

It loads a local .env file during development. In Deno Deploy, use runtime environment variables instead of committing the file or embedding secrets in source code.

Can I put the Volanea API key in a frontend application?

No. Keep the key on the server only. A browser bundle, mobile application, or public client can be inspected, which would expose the credential to users and attackers.

Does an accepted API request guarantee inbox placement?

No. API acceptance means the sending request was accepted for processing. Inbox placement depends on sender-domain configuration, recipient mailbox policies, message content, recipient engagement, and other delivery factors.

Should I use this public test endpoint in production?

No. Use it only for controlled integration testing, then protect, remove, or replace it with an authenticated application workflow. A publicly callable send route can be abused even if it uses a fixed recipient.