Send email from Heroku by making a server-side HTTPS request from your application to Volanea’s REST API. Heroku is an application platform rather than an automation product with record-created triggers, so the event that starts the email is an event in your app—for example, a form submission, a completed checkout, a newly created user, or a changed order status.
This distinction matters. There is no native Volanea Heroku app, Marketplace installation flow, or Heroku workflow builder that forwards a platform-defined payload to an email provider. Instead, a Heroku web dyno or worker dyno runs your code; that code decides whether an app event deserves an email and calls Volanea over HTTPS. That is a useful model because it keeps business rules, secrets, validation, and retries under your control.
This guide uses a Node.js and Express example because it makes the request boundary clear, but the architecture applies to Rails, Django, Flask, Laravel, Spring, Go, and other applications deployed on Heroku. For endpoint details and the full request reference, keep the email API setup guides nearby while you implement the production version.
What actually triggers an email on Heroku
A common source of confusion is treating Heroku like a CRM or no-code database. Heroku does not create a universal “record created” event, nor does it send an outbound webhook payload whenever application data changes. It hosts the process that receives requests and executes your application code.
For this guide, the concrete trigger is an HTTP form submission to an Express route. A visitor submits a signup form to POST /signup; the Heroku router forwards that request to your web dyno; your app validates the fields and stores the user. After the database write succeeds, your application creates a welcome-email job.
That is the right trigger point because the user record, not a browser click alone, is the business event. If a request is retried, the user insert and email decision need a durable identifier. If the browser closes after receiving a response, the email can still be sent by a worker. And if a database constraint rejects the signup, no welcome email should be queued.
The request Heroku receives
The following JSON is an example of the payload your application receives from its own frontend or another trusted server. It is not a special Heroku webhook format; Heroku does not manufacture this payload.
{
"email": "ada@example.com",
"firstName": "Ada",
"marketingOptIn": false
}
Your route should regard every field as untrusted input. The email address must be checked before use, the name must be escaped before it enters HTML, and the client must never choose the sender address, API key, HTML template, or idempotency key. Those are server-side decisions.
Why an application event is better than a client-side send
A browser should ask your backend to create an account or place an order. It should not call an email API directly. If it did, anyone could inspect the client bundle or browser network traffic and recover credentials capable of sending mail under your domain.
A server-side flow also lets you make email conditional on durable state. For example, a welcome email should go out only after a newly created user is committed, a receipt should go out only after payment is confirmed, and an invitation should go out only after an invite token exists. The trigger is therefore usually one of these application transitions:
- a user row is successfully created;
- an order moves to
paid; - a password-reset token is generated;
- an administrator creates an invitation;
- a support case changes to a customer-visible status; or
- a worker finds a scheduled notification that is due.
The important rule is simple: choose an event with a stable business ID. “The web request ran” is not a stable event; user:123 or order:ord_456:receipt is.
The recommended architecture: web dyno, database, worker
It is technically possible to call Volanea directly inside an Express route after a form submission. For low-volume, noncritical notifications, that can be acceptable. But tying a user-facing request to an external email call means a temporary provider delay can slow the page response, consume web-dyno concurrency, and complicate recovery after timeouts.
A more reliable pattern separates acceptance from delivery:
- The web dyno validates the request and commits the business change.
- In the same transaction or immediately afterward, it records an email job with a stable event identifier.
- A worker dyno claims pending jobs and sends them to Volanea.
- The worker saves the accepted result or schedules a bounded retry.
- A delivery-event webhook, if you configure one, updates your application’s view of delivery outcomes.
Heroku’s guidance for scheduled and background work similarly separates scheduling from executing work. In practice, the same principle prevents a slow API call from occupying the request that a real person is waiting on.
A minimal job record
Your exact schema depends on your database, but an email outbox table often needs fields like these:
| Field | Purpose |
|---|---|
id | Internal job identifier. |
event_key | Stable unique business key, such as welcome:user_123. |
email_type | A value such as welcome, receipt, or password_reset. |
recipient | Recipient address after validation. |
payload_json | The data needed to render the message. |
status | pending, sending, accepted, retrying, or failed. |
attempt_count | Supports retry limits and operations review. |
provider_message_id | The message ID returned after Volanea accepts the send. |
last_error | Sanitized failure context, never a secret. |
Put a unique database constraint on event_key. That constraint is your first duplicate defense: even if two web processes receive nearly identical signup requests, only one welcome-email job should be created for the logical event.
Set Volanea credentials as Heroku config vars
The Volanea API key belongs in a Heroku config var. Config vars are exposed to server-side application processes as environment variables, persist across deploys and restarts, and can be managed with the CLI or in the app’s Settings area in the Heroku Dashboard.
Set a distinct key and sender for each Heroku app environment. A staging app should not silently send from the same production configuration, and production should not inherit test credentials from a checked-in .env file.
heroku config:set \
VOLANEA_API_KEY='replace-with-your-server-side-key' \
VOLANEA_FROM='Acme <hello@updates.example.com>' \
-a your-heroku-app
Your sending address must use a domain that has been authenticated in Volanea. Do not put an unverified address into code and assume it will be accepted at send time; make domain authentication part of deployment readiness.
Why the key must never be client-visible
A Heroku config var is safe only when it is read by a server-side process. It stops being safe if you expose it through any of the following paths:
- a Vite, Next.js, React, or other frontend build variable that is bundled for browsers;
- a JSON configuration endpoint available without authentication;
- a browser-side
fetch()call that includesAuthorization: Bearer ...; - a log line, error response, screenshot, support ticket, or copied curl command; or
- source control, including an old commit that later gets deleted from the working tree.
The client receives only a generic result such as { "ok": true }. The browser never needs the email provider key, and it should not learn whether an individual recipient address was suppressed, invalid, or unsubscribed.
If a key is accidentally exposed, revoke or rotate it promptly, update the relevant Heroku config var, and redeploy or restart the app process as needed. Do not treat a new Git commit that removes the key as remediation; the old secret may remain in repository history, build logs, copied artifacts, or browser bundles.
Send a Volanea email from a Heroku worker
Volanea’s single-message endpoint is POST https://api.volanea.com/v1/send. Authenticate with a Bearer API key, send JSON, and use Idempotency-Key to associate retries with one logical email rather than one HTTP attempt.
The code below includes the full mapping from a queued signup email job to a Volanea message. It uses Node’s built-in fetch, available in current Node runtimes, and keeps the API key entirely on the server.
// worker.js
// Run this in a Heroku worker dyno, not in browser code.
import crypto from "node:crypto";
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
async function sendWelcomeEmail(job) {
// Example job created after a successful signup:
// {
// id: "job_01J...",
// event_key: "welcome:user_123",
// recipient: "ada@example.com",
// payload_json: { firstName: "Ada", userId: "user_123" }
// }
const firstName = job.payload_json.firstName?.trim() || "there";
const safeFirstName = escapeHtml(firstName);
// This is the Volanea REST payload. The app controls sender,
// subject, content, and tags; the browser does not.
const emailPayload = {
from: process.env.VOLANEA_FROM,
to: [job.recipient],
subject: "Welcome to Acme",
html: `<p>Hi ${safeFirstName},</p><p>Your account is ready.</p>`,
text: `Hi ${firstName},\n\nYour account is ready.`,
tags: ["welcome", "signup"]
};
const response = await fetch("https://api.volanea.com/v1/send", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.VOLANEA_API_KEY}`,
"Content-Type": "application/json",
// Stable for this business event. Reuse exactly this value on retries.
"Idempotency-Key": job.event_key
},
body: JSON.stringify(emailPayload),
signal: AbortSignal.timeout(10_000)
});
const responseBody = await response.json().catch(() => ({}));
if (!response.ok) {
const error = new Error(`Volanea send failed with HTTP ${response.status}`);
error.status = response.status;
error.responseBody = responseBody;
throw error;
}
return responseBody;
}
async function processJob(job, db) {
try {
const result = await sendWelcomeEmail(job);
// Save the provider response or its returned message identifier.
// A successful API response means accepted/queued, not necessarily delivered.
await db.emailJobs.markAccepted(job.id, {
providerResponse: result
});
} catch (error) {
const retryable = !error.status || error.status >= 500 || error.status === 429;
if (retryable && job.attempt_count < 5) {
await db.emailJobs.scheduleRetry(job.id, {
// Exponential backoff is an example; calculate from attempt count in production.
nextAttemptAt: new Date(Date.now() + 60_000),
lastError: error.message
});
return;
}
await db.emailJobs.markFailed(job.id, {
lastError: error.message
});
throw error;
}
}
The mapping is intentional:
job.recipientbecomes Volanea’stoarray.VOLANEA_FROMbecomesfrom, keeping sender identity outside user input.firstNamebecomes both an escaped HTML value and a plain-text fallback.- fixed tags classify the email for later operational analysis.
job.event_keybecomesIdempotency-Key, so a retried job identifies the same send.
Do not replace job.event_key with crypto.randomUUID() inside sendWelcomeEmail. A new random value on every attempt defeats idempotency because the email API sees each retry as a new logical operation. Generate a random job identifier when a new job is created if you need one, store it durably, and reuse it thereafter.
Create the job from the form-submission route
The web route below shows the precise trigger. POST /signup is the HTTP form submission that begins the flow. The handler validates input, creates a user, and records an outbox job in the same database transaction before responding to the browser.
import express from "express";
import { db } from "./db.js";
const app = express();
app.use(express.json());
app.post("/signup", async (req, res, next) => {
try {
const email = String(req.body.email || "").trim().toLowerCase();
const firstName = String(req.body.firstName || "").trim();
if (!email || !email.includes("@")) {
return res.status(422).json({ error: "Enter a valid email address." });
}
const user = await db.transaction(async (tx) => {
const createdUser = await tx.users.create({ email, firstName });
await tx.emailJobs.insert({
event_key: `welcome:${createdUser.id}`,
email_type: "welcome",
recipient: createdUser.email,
payload_json: {
userId: createdUser.id,
firstName: createdUser.firstName
},
status: "pending",
attempt_count: 0
});
return createdUser;
});
// The worker sends later. Do not reveal API behavior to the browser.
return res.status(201).json({
id: user.id,
message: "Account created. Check your inbox shortly."
});
} catch (error) {
return next(error);
}
});
In a real application, make the unique event_key constraint part of the database schema. If users.create sees a repeat signup, decide your product behavior explicitly: return the existing account flow, request verification again, or avoid revealing account existence. Do not use an email-send failure as a reason to undo a successfully created user account unless email delivery is truly a hard business requirement.
Configure the Heroku process types
Heroku runs commands declared in a Procfile. Put the request-serving code in a web process and the email queue consumer in a worker process. This isolates interactive traffic from background email work and lets you scale them independently.
web: node server.js
worker: node worker.js
A worker should continuously claim one pending job at a time, mark it as being processed with an atomic database update, call Volanea, and then save the result. That claiming step matters when you run multiple worker dynos. Without it, two workers can read the same pending job and both send it.
Use database locking or an atomic UPDATE ... WHERE status = 'pending' strategy appropriate to your database. A job queue library can help, but a library does not eliminate the need for a stable idempotency key. Processes can crash after Volanea accepts a request but before your worker records success. On restart, the job may be attempted again; the same Idempotency-Key gives the provider a way to recognize that retry.
Scheduled email is a different trigger
For reminders, daily summaries, or renewal notices, the concrete trigger can be a scheduled job rather than a form submission. Heroku Scheduler runs configured commands at coarse intervals, while a custom clock process offers more control for more demanding schedules.
Do not make a scheduler command directly send every email in a long loop. Let the scheduler identify due records and enqueue email jobs. Workers should then perform the sends. That limits the blast radius of a partial failure and keeps the same audit trail, retry rules, and idempotency model used for event-driven email.
When this breaks: failures at the Heroku-to-Volanea hop
Email integrations fail in ambiguous ways. A failure response is easy: the API did not accept the request. The harder case is a timeout or process interruption: your app cannot tell whether the request never left Heroku, reached Volanea but did not return a response, or completed while the worker died before saving success.
Design for ambiguity instead of assuming exactly-once execution.
Retries can create duplicate sends
Heroku can restart dynos during deploys, maintenance, crashes, or resource events. Your job runner might retry after a networking error. You might manually rerun a failed task. A queue library might deliver a job at least once. Each of those paths can invoke the send operation more than once.
The protection is layered:
- Make
event_keyunique in your database so the same business event creates one job. - Claim jobs atomically so concurrent workers do not process one row at the same time.
- Use the same
Idempotency-Keyfor every retry of that job. - Record the accepted provider response before marking the job complete.
- Alert on repeated retries rather than silently retrying forever.
An idempotency key is scoped to the logical email. receipt:order_501 and shipment:order_501 must be different because they are different messages. A second receipt for a legitimate partial shipment should also use a distinct event key, such as shipment:shp_993:receipt.
Webhook and request timeouts
Heroku’s router has a 30-second request timeout for inbound web requests. If your signup request waits on a slow database query, third-party call, or email API response, the router can terminate the client-facing transaction while the dyno continues working. From a visitor’s perspective, the action may look failed even though your server eventually creates the user or sends the message.
Avoid this outcome by making web requests fast. Commit the durable business state and outbox job, return a response, and move the Volanea call to a worker. Set a shorter timeout for the outbound API call—10 seconds in the sample—so a stuck dependency does not consume worker capacity indefinitely.
If you later configure Volanea event webhooks back to your Heroku app, use the same principle in reverse. Verify the webhook, persist the useful event data quickly, return a successful response promptly, and process expensive follow-up work asynchronously. Do not download reports, render PDFs, or make several slow provider calls before acknowledging an inbound webhook.
Payload fields may not be present
Because the trigger in this guide is your own route, field availability is your application contract. A public signup form might omit firstName; an API client might send null; an older mobile client might send an unexpected shape. Treat optional data as optional and provide a safe fallback.
This concern becomes more acute when the app receives webhooks from another service. Different event types may have different fields, test payloads may be smaller than live payloads, and feature or plan differences in the source system can affect what data is included. Do not assume that a nested field exists just because it appeared in one test event.
Validate required fields at the boundary, store the source payload or selected fields according to your privacy policy, and make recipient lookup explicit. For example, if a billing webhook lacks a customer email, look it up from your own customer record using a trusted customer ID. Do not send a receipt to an address provided by an arbitrary unverified webhook field.
A successful send response is not delivered mail
A successful POST /v1/send response means Volanea accepted the request for processing. It does not prove that the receiving mailbox accepted, displayed, or was read by the recipient. A message can later bounce, be suppressed, be filtered, or be rejected downstream.
Treat API acceptance and delivery as separate states. Your product should normally tell users “check your inbox” after acceptance, not “email delivered.” For critical flows such as account verification or password recovery, add observability around provider events, bounces, and complaints; expose a safe resend path; and give support staff enough information to diagnose the status without exposing raw email content or credentials.
Security and deliverability details worth keeping
The email API request is only one part of a production integration. The surrounding controls determine whether it remains reliable when traffic, attackers, or recipient behavior become less friendly.
Protect the signup route
A public form that immediately generates welcome mail can be abused to send unwanted email. Apply rate limits, bot defenses appropriate to your application, and sensible validation. Consider email verification before granting sensitive access, but avoid requiring a full delivery result inside the initial web request.
Never interpolate raw form fields into HTML. The example uses HTML escaping for a name; production templates should apply context-aware escaping for every user-controlled interpolation. Keep URLs generated server-side and validate any redirect destination before including it in a message.
Authenticate the sending domain
Use a sender address from a domain authenticated in Volanea, with the DNS records required by the domain setup. Domain authentication supports alignment and helps receiving systems establish that your application is authorized to use that domain for mail.
Separate sender identities by purpose when that helps operations. For example, transactional account mail might use accounts@updates.example.com, while order mail uses orders@updates.example.com. The address should still be recognizable to the recipient, monitored for replies when applicable, and consistent with the message’s purpose.
Keep logs useful but private
Log enough to correlate a signup, job, API response, and later delivery event. Good log fields include the internal job ID, event key, message type, attempt count, HTTP status, and a provider message identifier. Avoid logging the Bearer token, the complete recipient address where it is unnecessary, password-reset URLs, or entire HTML bodies.
A useful pattern is to log a hashed or partially masked recipient for routine operations while keeping authorized support lookup in the application database. That makes ordinary logs safer without making an email incident impossible to investigate.
Direct REST API versus SMTP on Heroku
Volanea supports both a REST API and SMTP access, but REST is especially well suited to application events that need structured request bodies, response handling, HTTP timeouts, idempotency headers, tags, and modern observability.
SMTP can be a practical choice when an existing framework or legacy library already has a mature mailer abstraction. However, SMTP does not naturally expose the same application-level idempotency header used in this guide. If duplicate prevention matters, preserve the durable outbox and deduplication design regardless of which transport you use.
For a new Heroku integration, REST keeps the sending intent explicit: your code sends a JSON message, attaches an idempotency key, parses a JSON response, and saves the result. That makes it easier to test with a mock HTTP server and easier to distinguish provider acceptance from later delivery outcomes.
A production checklist before you deploy
Before scaling worker dynos or enabling a customer-facing flow, review the implementation against this checklist:
- The Volanea API key is stored in a Heroku config var, never in source code or browser code.
- The sender domain is authenticated and the
fromvalue is controlled by server configuration. - The form or upstream webhook payload is validated before a job is created.
- Every logical email has a durable, unique
event_key. - The same
event_keyis sent asIdempotency-Keyon every retry. - Email jobs are persisted before the user-facing request returns.
- Workers claim jobs atomically and use bounded retries with backoff.
- Web requests do not wait for slow third-party delivery work.
- HTML uses escaped dynamic values and every message includes a useful text alternative.
- Logs exclude API keys, reset tokens, and unnecessary message content.
- Acceptance, bounce, suppression, and delivery outcomes are not treated as the same state.
- Staging and production use separate credentials and intentional sender settings.
Conclusion
To send email from Heroku with Volanea, do not look for a native add-on or a no-code platform trigger that does not exist. Put the integration in the application you already deploy on Heroku: let a meaningful business event create a durable email job, let a worker send the message over Volanea’s REST API, store the API key in a server-only config var, and reuse a stable idempotency key whenever work is retried.
That design is more than a way to get a welcome email out. It gives password resets, receipts, invitations, alerts, and scheduled notices the same reliable execution model. The result is an email system that stays understandable when a dyno restarts, a client retries, an upstream payload changes, or an external API call becomes temporarily uncertain.
FAQ
Does Volanea have a native Heroku add-on or Marketplace app?
No. The integration is a server-side REST API call from code running in your Heroku application or worker process. There is no native Volanea Heroku Marketplace installation flow required for this approach.
Where should I store the Volanea API key on Heroku?
Store it as a Heroku config var such as VOLANEA_API_KEY. Read it only from server-side code through process.env.VOLANEA_API_KEY; never expose it in frontend bundles, public configuration endpoints, or browser requests.
Should my Express route call the email API directly?
It can for simple, low-risk notifications, but a durable outbox plus worker is usually better. It keeps the form response fast, supports retries, and reduces the chance that a user-facing request times out while waiting for an external service.
How do I prevent duplicate emails after a retry?
Use a stable, stored event key such as welcome:user_123 for one logical email. Enforce uniqueness for that key in your database and send the exact same value in Volanea’s Idempotency-Key header on every retry.
Does a successful API response mean the recipient received the message?
No. It means the send request was accepted for processing. Delivery can later be affected by suppression, bounces, recipient-server behavior, or inbox filtering, so track delivery events separately for important workflows.