Send email with Java through Volanea by making an authenticated JSON request to the REST API from your backend. This guide uses Java 17’s built-in HttpClient, so the integration has no third-party SDK dependency and the request format remains transparent, testable, and portable.
What this Java integration sends
The example in this guide sends one transactional email: a single application-triggered message such as a password reset, receipt, verification email, account alert, or invitation. It calls Volanea’s POST /v1/send endpoint at https://api.volanea.com, authenticates with a secret API key, and supplies the sender, recipient, subject, and HTML content as JSON.
A transactional request is not the same thing as a campaign send. Your application owns the triggering event and should call the API only after it has completed the underlying business action. For example, create an order before sending an order confirmation; create a password-reset token before emailing its URL. That ordering makes your application’s state the source of truth even if an email request has to be retried.
Volanea’s send endpoint accepts one recipient or a limited list of recipients in a single send request. For normal transactional flows, send to the individual affected by the event rather than reusing an application event to email a broad audience. The API also supports an Idempotency-Key header, which is important when a network timeout leaves your application unsure whether the request reached the service.
This guide deliberately does not use a made-up Volanea Java SDK. A REST call with Java’s standard HTTP client is the most direct way to integrate when you need a working Java implementation and a stable understanding of every header and field being transmitted.
Requirements before you send
You need the following before running the example:
- Java 17 or later. Java 17 includes the
java.net.http.HttpClientused below. - A Volanea secret API key. Keep it on the server only and load it from an environment variable.
- A sender address on a domain you control. Use an address associated with a domain you have configured for sending in Volanea.
- A recipient address you can access while testing. This lets you validate both the API response and the mailbox result.
- Outbound HTTPS access from the environment where your Java process runs.
The important operational prerequisite is sender-domain setup. A valid JSON request does not make an arbitrary From address usable. Your sending identity needs to be configured so receiving providers can evaluate its authentication and alignment correctly. Start with the API reference and setup guides if you still need to configure your sending domain or generate a key.
Install Java
This guide has no Volanea SDK or JSON-library dependency to install. It uses the Java standard library only.
If Java 17+ is not already installed on macOS with Homebrew, use this exact command:
brew install openjdk@21
On systems where Java is already managed by your organization, confirm the installed version instead:
java --version
The sample uses only standard Java classes, so there is no Maven, Gradle, or third-party dependency version to keep in sync. That is useful for small services, command-line jobs, scheduled tasks, and applications where adding another runtime package is undesirable.
Set the API key as an environment variable
Never paste a live secret key into a Java source file, commit it to Git, or expose it in browser code. Set VOLANEA_API_KEY in the environment of the backend process.
macOS and Linux shells:
export VOLANEA_API_KEY="sk_your_secret_key"
PowerShell:
$env:VOLANEA_API_KEY = "sk_your_secret_key"
Windows Command Prompt:
set VOLANEA_API_KEY=sk_your_secret_key
For local development, put this variable in your shell profile or your application’s secret-management workflow. In deployment, inject it through the platform’s encrypted secrets facility. A secret key authorizes email sending, so treat it like a database password: scope access tightly, rotate it if exposure is suspected, and avoid logging it.
Complete Java example
Create a file named SendEmail.java, replace the example sender and recipient addresses, set VOLANEA_API_KEY, then compile and run it. The sender must be an address on a domain configured for sending in your Volanea account.
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.UUID;
public class SendEmail {
private static final String API_URL = "https://api.volanea.com/v1/send";
public static void main(String[] args) throws IOException, InterruptedException {
String apiKey = System.getenv("VOLANEA_API_KEY");
if (apiKey == null || apiKey.isBlank()) {
throw new IllegalStateException(
"VOLANEA_API_KEY is not set. Export it before running this program."
);
}
String from = "Acme <hello@your-verified-domain.com>";
String to = "you@example.com";
String subject = "Welcome to Acme";
String html = "<h1>Welcome</h1><p>Your Java email integration is working.</p>";
String payload = """
{
"from": "%s",
"to": ["%s"],
"subject": "%s",
"html": "%s"
}
""".formatted(
jsonEscape(from),
jsonEscape(to),
jsonEscape(subject),
jsonEscape(html)
);
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Idempotency-Key", UUID.randomUUID().toString())
.POST(HttpRequest.BodyPublishers.ofString(payload, StandardCharsets.UTF_8))
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
);
int status = response.statusCode();
if (status < 200 || status >= 300) {
throw new IOException(
"Volanea send failed with HTTP " + status + ": " + response.body()
);
}
System.out.println("Volanea accepted the email request.");
System.out.println("HTTP status: " + status);
System.out.println("Response: " + response.body());
}
private static String jsonEscape(String value) {
return value
.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\b", "\\b")
.replace("\f", "\\f")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
}
Compile and run it with:
javac SendEmail.java && java SendEmail
A successful 2xx response means Volanea accepted the request for processing. It is not the same as proof that the recipient has opened the message, nor is it necessarily final evidence of mailbox delivery. Store the returned response in structured logs with your internal event or order identifier, then use event delivery data and webhooks to observe subsequent processing outcomes.
Understand the request fields
The request body in the example is intentionally small:
{
"from": "Acme <hello@your-verified-domain.com>",
"to": ["you@example.com"],
"subject": "Welcome to Acme",
"html": "<h1>Welcome</h1><p>Your Java email integration is working.</p>"
}
from identifies the visible sender. Use a recognizable display name and an address from your configured sending domain. The display name is optional from an email-protocol perspective, but it generally makes transactional messages easier for recipients to recognize. Keep it stable across related messages so users can identify your application in their inbox.
to is an array in this example, even though it contains one address. Arrays are useful because the send endpoint supports more than one recipient in a request. For user-specific messages such as sign-in alerts, receipts, or reset links, an array with one address is normally the safest model: it avoids accidentally exposing recipients to one another and avoids treating a transactional action as a broadcast.
subject should describe the user’s action or the information requiring attention. Avoid generic subjects such as “Notification” when a precise alternative exists. “Your Acme password reset link” is more useful than “Password reset,” and “Receipt for order #4821” is more useful than “Order update.”
html contains the HTML version of the email. Email HTML is more constrained than normal web-page HTML: many clients have inconsistent CSS support, remove scripts, and alter markup. Use simple table-compatible layouts where needed, inline important styles, include a plain visual hierarchy, and test the message in the clients used by your audience.
Use trusted content and escape dynamic values
The sample’s jsonEscape method prevents dynamic Java strings from breaking the JSON document. It does not make untrusted user data safe to insert into HTML. Those are separate problems.
If values such as a customer name, product name, or note appear inside the html field, HTML-escape them before inserting them into markup. If you do not, a value containing characters such as <, >, &, or quotes can alter the email’s HTML. More seriously, never insert raw user-provided HTML into a transactional message unless your product intentionally supports it and you have a robust sanitization policy.
A practical split is:
- Escape dynamic values for HTML when placing them inside the email body.
- Build the final HTML document.
- Escape the completed Java string for JSON when building the request body.
- Send only from your trusted backend environment.
For more complex messages, use a JSON library in your production application rather than concatenating JSON manually. The dependency-free example is designed to be copy-pasteable, but a mature service usually already has Jackson, Gson, JSON-B, or a framework serializer available. The important rule is the same: let a serializer create JSON, and let an HTML encoder handle untrusted data before it reaches the email template.
Authentication and HTTP headers
Volanea authenticates this REST request with a bearer token in the Authorization header:
Authorization: Bearer sk_your_secret_key
The API key is a server credential. Do not use this Java code in an Android app, a desktop client distributed to users, a browser application, or any environment where the key can be extracted. Instead, expose a protected endpoint in your own backend and let that backend decide whether the user action is authorized to trigger an email.
The request also explicitly sends:
Content-Type: application/json
Accept: application/json
Content-Type tells the API how to parse the request body. Without it, or with a value intended for a different payload format such as application/x-www-form-urlencoded, the service may reject or misinterpret the body. Accept states that the Java client expects JSON in return. While APIs may default responses sensibly, declaring both headers removes ambiguity and makes troubleshooting easier.
Why the example adds an idempotency key
The Idempotency-Key header associates one logical send operation with a unique value. The example generates a UUID:
.header("Idempotency-Key", UUID.randomUUID().toString())
That is correct for a one-off test. In production, generate the key once for the underlying business event and persist it with that event or delivery job. For example, use a value derived from a durable notification record such as password-reset:notification-9812, or store a generated UUID in the notification table before attempting the send.
Do not generate a new idempotency key for every retry of the same message. A fresh key tells the API this is a new logical operation and can allow a duplicate send. Reuse the original key only for retries of the same payload and event. Generate a new key when the application deliberately intends to send a separate message.
Make transactional sends reliable
A basic API call is only one part of a reliable email workflow. Your Java application has to make a decision when the network, process, or upstream service behaves unexpectedly. The safest architecture records the intent to send before making the HTTP request and records the outcome afterward.
Recommended application flow
For an event such as a completed order, use a sequence like this:
- Commit the order and its line items to your database.
- Create a notification record with a stable internal ID and a
pendingstate. - Generate and persist an idempotency key for that notification.
- Build the email payload from the committed data.
- Send the request to Volanea.
- Save the HTTP status and response details without storing the secret key.
- Mark the notification as accepted only after a successful 2xx response.
- Retry transient failures with the same idempotency key and bounded backoff.
This pattern prevents a common failure mode: an order exists, the process crashes, and the application no longer knows whether it sent the receipt. A durable notification record lets another worker resume safely. It also gives your support team an auditable trail when a customer asks whether a message was sent.
Decide what to retry
A timeout or connection failure does not always mean the send failed. The request could have reached the API while the response was lost. That is exactly the situation where the persisted idempotency key matters.
Retry only failures that are plausibly temporary, such as connection resets, timeouts, or selected 5xx responses. Avoid automatic retries for malformed JSON, an invalid sender, a missing recipient, or authentication failures; those require a configuration or code fix. Apply exponential backoff and a maximum attempt count so a faulty deployment does not generate a tight retry loop.
The sample uses a 10-second connection timeout and a 30-second overall request timeout. Those values are reasonable starting points, not universal production defaults. Choose limits based on your service’s latency budget and use a queue or background worker for messages that do not need to delay an interactive request.
Keep user-facing requests fast
For a password reset or verification flow, a user should not have to wait for an email provider’s full response before receiving an HTTP response from your web application. A synchronous send is useful for development and simple integrations, but a production service often benefits from putting a notification job on a queue.
The web request can validate the action, create the durable notification record, enqueue work, and return. A worker then executes the Java send operation. This separates product responsiveness from delivery-provider latency and gives you one place to implement retries, rate controls, logs, and alerting.
Use Java HTTP clients correctly
The example calls client.send(...), which blocks the current thread until the response arrives or the configured timeout expires. That simplicity makes it well suited to a command-line test or a worker thread.
Java also offers sendAsync(...), which returns a CompletableFuture. Use it only when the surrounding application is designed to manage asynchronous completion, exceptions, timeouts, and shutdown behavior. Starting an asynchronous request and letting a command-line program exit immediately is a common cause of messages that appear not to send.
Here is the essential asynchronous pattern:
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenAccept(response -> {
if (response.statusCode() >= 200 && response.statusCode() < 300) {
System.out.println("Accepted: " + response.body());
} else {
System.err.println("Send failed: " + response.statusCode() + " " + response.body());
}
})
.exceptionally(error -> {
error.printStackTrace();
return null;
})
.join();
join() is included here so a short-lived program waits for completion. In a long-running application server, you may instead compose the future into your service’s existing asynchronous workflow. Do not ignore the exceptional completion path: DNS failures, TLS problems, connect timeouts, and request timeouts are represented as failed futures rather than normal HTTP responses.
Test the integration safely
Start with a mailbox you control and a clearly marked subject such as Volanea Java integration test. Use a real configured sender domain rather than trying to test with a public address you do not own. Then check three separate layers:
- Application result: Did Java receive a 2xx status and response body?
- Provider processing: Did the request appear in your sending activity or event stream?
- Recipient result: Did the message arrive in the expected mailbox, and did it render correctly?
Those layers matter because “request accepted” and “message delivered to a mailbox provider” are different states. Spam filtering, recipient-side policies, user mailbox rules, and invalid addresses can affect what happens after acceptance.
When testing a dynamic message, test data boundaries as well as the happy path. Include long names, apostrophes, ampersands, non-ASCII characters, missing optional fields, and URLs with query parameters. These values reveal encoding mistakes that a simple Hello world email will not show.
Use test recipients only for test data. A real production API key can send real messages, so build environment-level safeguards: separate development and production keys, explicit test sender identities, and a recipient allowlist in non-production environments. Never point a development loop at a production customer list.
Common errors when sending email with Java
Authentication failures: HTTP 401 or 403
An authentication or authorization response usually means the key is missing, malformed, revoked, or not valid for the environment you are calling. First confirm that VOLANEA_API_KEY is present in the process environment rather than merely set in a different terminal session.
Print only whether the variable exists, never the value itself:
System.out.println(System.getenv("VOLANEA_API_KEY") != null);
Also confirm the header format is exactly Authorization: Bearer <key>. Do not send the key as a query parameter, do not place it in the JSON body, and do not include placeholder angle brackets around the key. If you rotate a key, update the deployment secret and restart or reload the application process that reads it.
Unsupported media type or invalid JSON: often HTTP 400 or 415
Set Content-Type to application/json and serialize valid JSON. A frequent Java mistake is using BodyPublishers.ofString(payload) without adding the content-type header, or constructing JSON with unescaped quote characters from dynamic values.
The complete example sets the header and escapes values for JSON. In a framework application, prefer a JSON serializer instead of string concatenation. If the response body includes validation details, log that body securely; it often identifies the invalid field more precisely than the status code alone.
Sender-domain or sender-address errors
If the API rejects from, verify that the address belongs to a domain configured for sending and that the exact local part is permitted by your sender configuration. Replacing the sender with a personal mailbox address may make a test feel convenient, but it usually undermines authentication and can be rejected.
Use a stable sender such as receipts@your-verified-domain.com or security@your-verified-domain.com. Keep transactional categories distinct when that helps recipients and your operations team understand the message stream.
The request succeeds but no email appears
First inspect the response from the API and then inspect event data or sending activity. Check the recipient address for typos, look in spam and promotions folders, and verify that the address is not suppressed due to a prior bounce, complaint, unsubscribe, or manual block.
Do not repeatedly resend the same message while investigating. That can create duplicates and worsen the recipient experience. Use the original idempotency key when retrying an uncertain attempt, and use an internal notification record to distinguish a legitimate retry from a new send.
Async mistakes with sendAsync
sendAsync does not throw normal HTTP error responses directly from the line where it is called. Its result arrives through a CompletableFuture. If you do not attach error handling, failures can be lost; if a short-lived Java program terminates immediately, it may end before the asynchronous operation completes.
Use .thenAccept(...) for successful completion, .exceptionally(...) for failures, and .join() or another lifecycle-aware waiting mechanism when appropriate. For many transactional workflows, a background job with synchronous send(...) is easier to reason about than unmanaged asynchronous calls.
Treating every non-2xx response the same
A 4xx response generally signals a request, configuration, or authorization problem. Retrying it unchanged is unlikely to help. A 5xx response or a transport failure can be temporary, but retry it with the same persisted idempotency key and bounded backoff.
Always capture the HTTP status, response body, your internal notification ID, and a safe correlation value in logs. Do not log email HTML containing sensitive information, full API keys, reset tokens, or complete recipient data unless your security policy explicitly permits it.
Improve the message beyond the first send
After the first successful request, improve the email as a product surface rather than treating it as an infrastructure afterthought. Include a useful subject, a recognizable sender, a short plain-language explanation, and a clear action where appropriate. A password-reset message should state why it was received, how long the link is valid if your product has an expiry, and what to do if the recipient did not request it.
For HTML content, design for constrained email clients. Avoid JavaScript entirely, avoid relying on advanced CSS behavior, provide meaningful link text, and use absolute HTTPS URLs for images and actions. Keep critical content as actual text rather than text rendered inside an image.
Also consider privacy. Receipts, security alerts, and account messages can reveal sensitive details when a mailbox is shared or compromised. Include only what a recipient needs. For example, a sign-in alert might show approximate time and location, while a password-reset email should not disclose whether a particular account exists to an unauthenticated requester.
Next steps: webhooks and templates
Once your first Java request works, add webhooks so your application can receive event notifications and update its own records when messages progress through processing. A webhook receiver should verify incoming requests according to the webhook configuration, return a quick success response, and process events idempotently because delivery systems can retry notifications.
Then move reusable message markup into templates. Templates reduce duplicated HTML across services, make brand updates safer, and separate application data from presentation. Your Java service can continue to own the transactional trigger and data while a template provides the reusable layout and content structure. Keep template changes reviewed and test them with representative data before using them for critical customer communications.
As these flows grow, build a small notification layer in your application: a durable message record, an idempotency key, a payload builder, a sender worker, webhook processing, and operational dashboards. That structure turns one successful API call into a dependable email system.
FAQ
Do I need a Volanea Java SDK to send email with Java?
No. This guide uses Java 17’s standard HttpClient to call Volanea’s REST endpoint directly. That avoids depending on an unverified SDK and keeps the request, headers, timeouts, and error handling explicit.
What Java version does this example require?
Use Java 17 or later. The code relies on the standard java.net.http.HttpClient API and Java text blocks, both available in modern Java releases.
Why should I use an idempotency key when sending email?
A timeout can leave your application uncertain whether the API received the request. Reusing the same idempotency key for a retry tells the API that it is the same logical send operation, helping prevent duplicate messages.
Can I call the Volanea API from browser JavaScript or a mobile app?
Do not call it directly from any client environment that would expose a secret API key. Send email from your backend, where the key can remain private and where you can enforce authorization, rate limits, logging, and retry behavior.
Does a 2xx response guarantee inbox placement?
No. A successful response means the API accepted the request. Final mailbox outcomes depend on later processing, sender authentication, recipient address validity, recipient-provider policies, and mailbox filtering. Use event handling and testing to observe the complete delivery path.