Send email with .NET through Volanea by calling the REST API from a standard HttpClient. This guide uses a .NET 8 console application, an API key stored in an environment variable, and a single POST /v1/send request to deliver one transactional message.

The integration deliberately uses Volanea’s HTTP API rather than a fictional provider-specific .NET SDK. That keeps the implementation portable, makes every outgoing request visible in your code, and works in console applications, ASP.NET Core services, worker processes, scheduled jobs, and queue consumers.

What this .NET integration sends

A transactional email is a one-to-one message triggered by an action or system event. Typical examples include password-reset links, account-verification messages, invoices, order confirmations, login alerts, invitations, and failed-payment notices.

The request in this guide sends one HTML email. It includes the minimum message information an email provider needs to process delivery:

  • A from address on a domain you are authorized to send from.
  • A recipient in to.
  • A subject line.
  • HTML content in html.
  • An API key in the HTTP Authorization header.

Volanea’s send endpoint is POST https://api.volanea.com/v1/send. The send API accepts one message and can address one recipient or up to 50 recipients in a request. Before testing the code, make sure your sending domain is configured and verified in Volanea. A successful API call means Volanea accepted the request for processing; it is not the same thing as a recipient opening the message or a mailbox provider placing it in the inbox.

Prerequisites

Before you send email with .NET, prepare the application and sending identity.

Install the .NET SDK

This example targets .NET 8. Confirm the SDK is available:

dotnet --version

Create a new console project:

dotnet new console --framework net8.0 --name VolaneaEmail
cd VolaneaEmail

Install the HTTP client factory dependency

Install Microsoft.Extensions.Http, which provides IHttpClientFactory and the AddHttpClient registration used by the complete sample:

dotnet add package Microsoft.Extensions.Http

HttpClient itself is part of modern .NET. The package above gives the application a managed, reusable way to create named clients instead of constructing a fresh HttpClient for every email. Reusing clients matters in web services and workers because frequent client creation can lead to avoidable socket exhaustion and makes centralized configuration harder.

Create and export an API key

Create a Volanea secret key, then store it outside the source code. Secret keys use the sk_... or sk_test_... form. Do not commit either form to a repository, paste it into frontend code, or place it in a checked-in appsettings.json file.

For macOS or Linux shells, set the variable for the current terminal session:

export VOLANEA_API_KEY="sk_test_replace_with_your_key"

For PowerShell on Windows:

$env:VOLANEA_API_KEY = "sk_test_replace_with_your_key"

For Windows Command Prompt:

set VOLANEA_API_KEY=sk_test_replace_with_your_key

Use a test key while developing if your Volanea account provides one, then supply the production secret through your deployment platform’s secret manager when you deploy. The code below reads the same VOLANEA_API_KEY variable in every environment, so promotion from development to production does not require changing application code.

Verify the sender before running the sample

Replace hello@your-verified-domain.example in the sample with an address on a domain you have configured for sending. A From address is not merely display text: mailbox providers evaluate the underlying domain’s authentication and alignment when they decide how to handle the message.

Use a recipient address you control for the first test. That lets you inspect the delivered message, its headers, and its rendering without accidentally sending development content to a real customer.

Complete working C# example

Replace the contents of Program.cs with the following code. It installs and uses the .NET HTTP client factory, loads VOLANEA_API_KEY from the environment, sets a Bearer authorization header, serializes the request as JSON, and sends one transactional email.

using System.Net.Http.Headers;
using System.Net.Http.Json;
using Microsoft.Extensions.DependencyInjection;

var apiKey = Environment.GetEnvironmentVariable("VOLANEA_API_KEY");

if (string.IsNullOrWhiteSpace(apiKey))
{
    throw new InvalidOperationException(
        "VOLANEA_API_KEY is not set. Export a Volanea secret key before running the app.");
}

var services = new ServiceCollection();

services.AddHttpClient("Volanea", client =>
{
    client.BaseAddress = new Uri("https://api.volanea.com/");
    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue("Bearer", apiKey);
    client.DefaultRequestHeaders.Accept.Add(
        new MediaTypeWithQualityHeaderValue("application/json"));
});

using var serviceProvider = services.BuildServiceProvider();
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var client = httpClientFactory.CreateClient("Volanea");

var email = new
{
    from = "Volanea Example <hello@your-verified-domain.example>",
    to = new[] { "your-inbox@example.com" },
    subject = "Your .NET email integration is working",
    html = """
        <!doctype html>
        <html lang="en">
          <body>
            <h1>It works</h1>
            <p>This transactional email was sent from a .NET application through Volanea.</p>
          </body>
        </html>
        """
};

using var response = await client.PostAsJsonAsync("v1/send", email);
var responseBody = await response.Content.ReadAsStringAsync();

if (!response.IsSuccessStatusCode)
{
    throw new HttpRequestException(
        $"Volanea returned {(int)response.StatusCode} ({response.ReasonPhrase}). " +
        $"Response: {responseBody}");
}

Console.WriteLine("Volanea accepted the email request.");
Console.WriteLine(responseBody);

Run the project:

dotnet run

The program prints the response body after Volanea accepts the request. Keep that output while you are integrating. It is useful when you need to correlate an application action with delivery activity, investigate a failed request, or determine whether the application reached the API at all.

How the request works

The important part of the integration is short, but each line solves a specific problem in a production-oriented HTTP client.

IHttpClientFactory creates the named client

services.AddHttpClient("Volanea", ...) registers a named HTTP client. The name is local to your application; it is not sent to Volanea. The configuration callback assigns the API base address and default request headers once, so later email sends only need to provide a relative endpoint path and request body.

The base address ends in a slash:

client.BaseAddress = new Uri("https://api.volanea.com/");

That trailing slash is intentional. With a relative path such as v1/send, .NET combines the values into https://api.volanea.com/v1/send. A missing slash can change URI resolution in surprising ways when an application later changes the base URL or endpoint path.

The API key is a Bearer credential

The sample builds the authorization header without putting the secret into a URL, log statement, or email payload:

client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", apiKey);

This results in an HTTP header shaped like this:

Authorization: Bearer sk_test_replace_with_your_key

Never send the key from browser JavaScript. A browser-delivered secret can be extracted by a visitor, extension, proxy, source-map inspection, or a compromised client device. Send transactional messages from server-side code only, where the key remains under your operational control.

PostAsJsonAsync sets JSON content correctly

The line below serializes the anonymous C# object as JSON and posts it to the send endpoint:

using var response = await client.PostAsJsonAsync("v1/send", email);

PostAsJsonAsync uses JSON request content and sets the request content type to application/json. That is preferable to manually building a JSON string for a first integration because it avoids malformed quotation marks, invalid escaping, and an omitted content type.

The sample uses lowercase anonymous-object property names—from, to, subject, and html—so the serialized JSON matches those names exactly. Do not casually rename those fields to C#-style names such as FromAddress or HtmlBody unless you also intentionally control how they are serialized and have checked the API reference.

The email payload, field by field

Understanding the payload will make it easier to adapt this initial send to a real application event.

from

The from value identifies the sender shown to the recipient. The example uses a display name plus an address:

"from": "Volanea Example <hello@your-verified-domain.example>"

Replace both the display name and address for your product. Keep the address on a sending domain you have verified. If you send receipts, a clear sender such as Billing <receipts@yourdomain.example> helps recipients recognize the message. For account-security alerts, use a dedicated and recognizable sender such as Security <security@yourdomain.example>.

Do not use a recipient-supplied email address as from. That pattern creates spoofing and alignment problems. If users need to reply to a message, choose a controlled reply handling process rather than impersonating the user as the sender.

to

The sample uses an array even though it contains one address:

"to": ["your-inbox@example.com"]

That makes the example easy to adapt when a legitimate transactional event needs multiple recipients. Be deliberate about adding recipients. A password reset or verification email should go only to the account owner; copying an administrator or support mailbox into such messages can expose private links or account information.

Validate and normalize addresses at the boundary where your application collects them. Address syntax validation is useful, but it does not prove that an inbox is deliverable or that a person consented to receive a particular class of message. For pre-send address checks in a signup or import workflow, use the email verification tool rather than treating a simple regular expression as a deliverability check.

subject

The subject should accurately describe the event and remain useful when viewed in a crowded inbox. “Reset your password” is better than “Important notification,” while “Your order #4821 receipt” is better than a generic “Thank you.”

Avoid inserting sensitive values into subject lines. Subjects can appear in device notifications, lock screens, forwarded message lists, and shared inbox views. A subject may mention that an action is required, but account numbers, one-time codes, detailed health data, and private document names should remain in the secured email body or application.

html

The html property contains the message body. Use semantic and conservative email HTML: headings, paragraphs, tables where necessary for layout, descriptive links, inline-safe styling, and a readable text hierarchy. Email clients vary substantially, so browser-perfect CSS is not a reasonable expectation.

The short HTML in this guide is intentionally uncomplicated. In a real receipt or invitation, build the message from trusted application data and HTML-encode every user-controlled value before interpolation. For example, never concatenate an unescaped display name, comment, support ticket title, or organization name directly into markup. Otherwise a value that looks harmless in a database can become broken markup or an injection issue in the resulting message.

Put the send behind an application boundary

The console program is useful for verifying credentials and message shape. Production applications should usually wrap the send call in an email service rather than leaving it in a controller, payment handler, or database repository.

A small abstraction gives you one place to handle logging, timeouts, response classification, test doubles, and future additions such as templates. For example, an ASP.NET Core app might define an ITransactionalEmailSender interface, then implement it with a typed or named HttpClient.

A good sending boundary should answer these questions:

  1. Which business event caused this message?
  2. Is the request safe to retry, or could retrying create a duplicate email?
  3. Which API response or error should be recorded for support and debugging?
  4. Should the caller wait for the result, or should a background worker process a durable job?
  5. What happens if sending fails after the business transaction has already committed?

For low-volume, non-critical notifications, awaiting the API request within an application handler may be sufficient. For receipts, password resets, subscription events, or other important mail, consider recording an outbound-email job in durable storage as part of the business workflow. A worker can then send the message and record the outcome independently of the request that created the event.

This pattern prevents a user-facing request from silently losing the email because a process restarted at the wrong moment. It also gives operations staff a clear retry mechanism that is based on a stored business event instead of trying to reconstruct what happened from logs.

Async and reliability considerations

The sample uses await because an HTTP call is I/O work. The thread should be free to handle other work while the request is in flight.

Await the send call

This is correct:

using var response = await client.PostAsJsonAsync("v1/send", email);

These patterns are risky or incorrect in typical application code:

// Do not block an async flow this way.
var response = client.PostAsJsonAsync("v1/send", email).Result;

// Do not start a task and discard it when the outcome matters.
client.PostAsJsonAsync("v1/send", email);

Blocking with .Result or .Wait() can contribute to deadlocks in application environments and reduces the scalability benefits of asynchronous I/O. Discarding the task means exceptions may go unobserved and the process may shut down before the request finishes.

Handle accepted versus delivered as separate states

The immediate API response tells your application whether the API request was accepted or rejected. It does not, by itself, prove final mailbox delivery, inbox placement, or recipient engagement.

Model the initial outcome as something like requested, accepted, or rejected. Later delivery-related information belongs to a separate lifecycle. This distinction improves support tooling: “we accepted your reset request” and “the recipient provider accepted the message” are not the same event.

Retry carefully

Not every failure should be retried automatically. A request rejected because a required field is missing will fail again until the payload changes. A network timeout or a temporary server-side failure may be retryable, but a retry can create duplicate customer emails if the first request actually reached the provider before the connection failed.

For transactional workflows, tie any retry decision to a stable business identifier, such as an order ID, password-reset issuance ID, or notification ID. Record that identifier with the outbound job. Before adding retry behavior or an idempotency mechanism, consult the current Volanea API reference and setup guides for the supported request options and exact field requirements.

Common errors when sending email with .NET

Most first-integration failures are straightforward once you inspect the HTTP status, response body, environment variables, and the exact JSON being sent. Keep the sample’s failure message during development because it prints both the status code and provider response.

Authentication failures: 401 or 403

An authentication failure generally means the API key is absent, invalid, malformed, revoked, or being used in the wrong environment.

Check the following:

  • Confirm VOLANEA_API_KEY is set in the same shell, container, process, or deployment environment that runs dotnet run.
  • Restart the application after changing an environment variable. A running process does not automatically reload its environment.
  • Make sure no leading or trailing whitespace was copied with the key.
  • Verify that the application sends Authorization: Bearer <key> and not a custom header invented by an old example.
  • Ensure a production deployment receives its production secret through its secret manager rather than a local test key.

Do not log the full API key while debugging. It is acceptable to log whether a variable was present, the key’s prefix, or a securely redacted form. If a key is exposed in a terminal capture, CI output, issue tracker, or commit, rotate it immediately.

Wrong content type: 400 or 415

A REST endpoint that expects JSON needs a JSON body and a matching content type. A frequent mistake is sending form data, a plain string, or a manually generated JSON payload without Content-Type: application/json.

The sample avoids that problem by using PostAsJsonAsync. If you replace it with PostAsync, create StringContent explicitly and specify JSON:

using System.Text;
using System.Text.Json;

var json = JsonSerializer.Serialize(email);
using var content = new StringContent(json, Encoding.UTF8, "application/json");
using var response = await client.PostAsync("v1/send", content);

Prefer PostAsJsonAsync unless you have a concrete reason to control serialization yourself. If you do serialize manually, inspect the resulting JSON in a safe development environment and verify that names, arrays, strings, and escaped HTML match the API’s expected schema.

A missing or unverified sender

If the request fails because of the sender, confirm that the from address is on a domain you have configured and verified for Volanea. Also verify that the local part and domain are spelled correctly. hello@yourdomain.example and hello@your-domain.example are different addresses, and an extra space inside an address can produce confusing validation results.

Do not try to solve a sender error by using a public mailbox address that you do not control. Authenticate the domain you intend to use for product email and keep sender identities consistent with the product name recipients recognize.

Async/await mistakes

If your app appears to send nothing and reports no error, verify that the method containing the send operation is awaited all the way back to the caller. In a web endpoint, return or await the task. In a background worker, await the operation before acknowledging the queue message.

Also avoid async void except for event handlers required by a framework. An async void method cannot be awaited by its caller, which makes error handling and orderly shutdown much harder.

Invalid recipient data

A failed recipient can be caused by malformed addresses, an empty to array, user input with whitespace, or an address that your application should not contact. Normalize obvious whitespace before creating the outbound request, but do not rewrite addresses in ways that change their meaning.

For messages that must reach a person, build product flows that can handle a typo gracefully: allow the user to correct the address, resend after confirmation, and provide a support path. Repeatedly retrying a known-invalid address wastes sending capacity and can make operational reporting noisy.

Timeout, DNS, or transient network errors

A client-side timeout does not prove that Volanea did not receive the request. The connection may have failed after the server accepted it but before the response reached your application. That is why retry design must be connected to your business record, not just to a caught HttpRequestException.

Log the exception type, the business event ID, a timestamp, and a redacted recipient identifier. Do not log full HTML bodies, reset URLs, API keys, or other sensitive content by default. Those details are often copied into centralized logs with broader retention and access than the application database.

Testing the integration safely

Start with a test key when available and a recipient inbox you own. Send a single message, then inspect three places: your application console output, the API activity available in your Volanea account, and the recipient inbox or spam folder.

Use a small testing checklist:

  • Confirm the environment variable is loaded without printing the secret.
  • Confirm the request receives a successful HTTP response.
  • Confirm the message reaches the controlled inbox.
  • Check that the sender name, sender address, subject, and HTML rendering look correct.
  • Test a deliberate failure, such as removing the key in a local-only environment, to make sure your application logs a useful error without leaking credentials.

Do not turn a transactional send endpoint into an unrestricted public form endpoint. A contact form should have server-side validation, abuse controls, rate limits, and recipient rules. Otherwise an attacker can use your application and email credentials to generate unwanted traffic.

Next steps: templates and webhooks

Once the direct HTML send works, move repeated layouts into templates. Templates let an application identify reusable content rather than carrying the full markup in every send request; Volanea’s template API stores reusable content addressed by templateId. That reduces duplication for messages such as receipts, welcome emails, and invitation emails, while keeping the application responsible for supplying the right event data.

Next, plan for webhooks as the event-handling side of email infrastructure. A webhook endpoint is an HTTPS route in your application that receives notifications about later email events. Treat it as an untrusted network entry point: verify the provider’s documented signature or authentication mechanism, acknowledge valid requests promptly, store the event safely, and make event processing idempotent because network systems can deliver the same event more than once.

Keep template rendering and webhook processing separate from the initial send path. The send path creates a message request; templates standardize content; webhook processing updates your system’s record of what happened after the request.

FAQ

Do I need a Volanea-specific .NET SDK to send email with .NET?

No. This guide uses the REST API directly with HttpClient, which is appropriate for .NET applications and avoids depending on an unverified provider-specific SDK. Install Microsoft.Extensions.Http for the IHttpClientFactory pattern used in the sample.

Where should I store the Volanea API key?

Use an environment variable for local development and a managed secret store in deployed environments. The application reads VOLANEA_API_KEY; never hard-code the value in C#, browser code, repositories, screenshots, or plaintext configuration committed to version control.

Why does the code use await when sending email?

The HTTP request is asynchronous I/O. await allows the runtime to use the thread for other work while the request is in progress and ensures that exceptions and response status are handled before the application continues.

Does a successful send response guarantee inbox placement?

No. A successful response indicates that the API accepted the request. Final delivery and inbox placement depend on subsequent email processing and recipient mailbox-provider decisions. Design your application to distinguish API acceptance from later delivery-related events.

Can I use this code in ASP.NET Core?

Yes. Register the named client in the application’s dependency-injection container, then inject IHttpClientFactory or a typed email-sender service into the controller, endpoint handler, worker, or background service that owns the transactional event.