Send email with Python through Volanea by making an authenticated JSON request from your backend. This guide uses Python’s requests library, an API key stored in an environment variable, and one copy-pasteable script that sends a transactional email.

What you will build

By the end of this guide, you will have a small Python program that does four useful things:

  1. Loads Volanea credentials and message settings from environment variables.
  2. Sends a POST request to Volanea’s transactional email endpoint.
  3. Provides both HTML and plain-text versions of a transactional message.
  4. Surfaces useful error details when the API rejects or cannot process the request.

The implementation deliberately uses standard HTTPS and JSON rather than assuming a Python-only Volanea SDK. That keeps the integration straightforward, makes it easy to inspect requests in logs, and avoids tying your application to an unverified client-library method name.

This is appropriate for transactional messages that are initiated by an application event: a verification email, password-reset link, purchase receipt, account alert, invitation, or status notification. Do not place this script in browser-side code. Your Volanea API key is a server credential and must remain on infrastructure you control.

Prerequisites

Before running the example, make sure you have the following:

  • Python 3.9 or later installed.
  • A Volanea API key available to your server process.
  • A sender address on a domain that is configured for sending in Volanea.
  • A real recipient address you can access for testing.
  • Network access from the machine or container running the script to Volanea’s API.

A sender address matters as much as the Python code. The from address must be one your sending setup permits. In practice, that means using an address on a domain you have authenticated rather than an arbitrary mailbox address. Authentication records and sending-domain status affect whether a provider accepts the request and whether mailbox providers trust the message after it is sent.

Keep the API key out of source control. Do not paste it directly into a Python file, commit it to a repository, or expose it through a frontend environment variable. Treat it like a password: anyone who obtains it may be able to send mail under your account.

For the current API contract, authentication requirements, and any account-specific setup details, consult the email API reference and setup guides. The Python code below uses the standard REST pattern: bearer authentication, a JSON request body, and a POST request to create an email send.

Install the Python dependencies

Install the two dependencies used in this guide:

python -m pip install requests python-dotenv

requests performs the HTTPS request. python-dotenv is used only for local development convenience: it loads values from a local .env file into the process environment. In a deployed environment, such as a container platform, CI job, or serverless function, set these variables through the platform’s secret-management or environment configuration instead of shipping a .env file.

You can confirm the installation with:

python -c "import requests; from dotenv import load_dotenv; print('Dependencies installed')"

If your system maps python to Python 2 or does not provide that command, use python3 consistently instead:

python3 -m pip install requests python-dotenv

Using python -m pip is usually safer than invoking pip directly because it installs packages into the interpreter that will run your script. It prevents a common setup problem where a package appears to install successfully but cannot be imported at runtime because it was installed into another virtual environment.

Optional: use a virtual environment

A virtual environment keeps project dependencies isolated from your system Python installation. Create and activate one before installing packages:

python -m venv .venv
source .venv/bin/activate

On Windows PowerShell, activate it with:

.venv\Scripts\Activate.ps1

Then install the dependencies inside that environment:

python -m pip install requests python-dotenv

Add .venv/ to .gitignore. A virtual environment is not required for the example to work, but it is the normal choice for application projects because it makes dependencies reproducible and avoids version conflicts between projects.

Configure environment variables

Create a file named .env in the same directory as the Python script. This file is for local development only and should never be committed.

VOLANEA_API_KEY=replace_with_your_api_key
VOLANEA_FROM=Acme Support <support@your-verified-domain.example>
VOLANEA_TO=you@example.com

Replace the values as follows:

  • VOLANEA_API_KEY: your server-side Volanea API credential.
  • VOLANEA_FROM: a sender identity associated with your configured sending domain.
  • VOLANEA_TO: an inbox you control while testing.

The display name portion of VOLANEA_FROM is optional, but it is useful when recipients should recognize the source of the message. The email address inside angle brackets is the important identity for sending-domain configuration.

Add the .env file to .gitignore immediately:

.env

For deployments, use the same variable names but set them in the runtime environment. For example, a shell session can export them before running the script:

export VOLANEA_API_KEY="replace_with_your_api_key"
export VOLANEA_FROM="Acme Support <support@your-verified-domain.example>"
export VOLANEA_TO="you@example.com"
python send_email.py

Environment variables keep secrets separate from application code. They also make it easier to use different keys and sender addresses in local development, staging, and production without editing files.

Send email with Python

Create a file named send_email.py and paste in the complete example below.

import os
import sys
from typing import Any

import requests
from dotenv import load_dotenv

# Load .env for local development. In production, configure these values
# through your hosting platform or secret manager instead.
load_dotenv()

API_URL = "https://api.volanea.com/v1/emails"


def required_env(name: str) -> str:
    """Return a required environment variable or stop with a clear error."""
    value = os.getenv(name)
    if not value:
        raise RuntimeError(f"Missing required environment variable: {name}")
    return value


def response_body(response: requests.Response) -> Any:
    """Return JSON when possible, otherwise return response text."""
    try:
        return response.json()
    except ValueError:
        return response.text


def main() -> None:
    api_key = required_env("VOLANEA_API_KEY")
    from_address = required_env("VOLANEA_FROM")
    to_address = required_env("VOLANEA_TO")

    payload = {
        "from": from_address,
        "to": [to_address],
        "subject": "Welcome to Acme",
        "html": """
            <h1>Welcome to Acme</h1>
            <p>Your account is ready.</p>
            <p>If you did not create this account, you can ignore this email.</p>
        """,
        "text": """Welcome to Acme\n\nYour account is ready.\n\nIf you did not create this account, you can ignore this email.""",
    }

    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
        "Accept": "application/json",
    }

    try:
        response = requests.post(
            API_URL,
            headers=headers,
            json=payload,
            # First value is connection timeout; second is read timeout.
            timeout=(3.05, 15),
        )
    except requests.exceptions.Timeout:
        print("Request timed out while contacting Volanea.", file=sys.stderr)
        sys.exit(1)
    except requests.exceptions.ConnectionError as error:
        print(f"Could not connect to Volanea: {error}", file=sys.stderr)
        sys.exit(1)
    except requests.exceptions.RequestException as error:
        print(f"Unexpected HTTP client error: {error}", file=sys.stderr)
        sys.exit(1)

    if not response.ok:
        print(f"Volanea returned HTTP {response.status_code}", file=sys.stderr)
        print(response_body(response), file=sys.stderr)
        sys.exit(1)

    print("Email accepted by Volanea.")
    print(response_body(response))


if __name__ == "__main__":
    main()

Run it from the same directory as .env:

python send_email.py

If the request succeeds, the script prints an acceptance message and the API response body. Save the returned response data in application logs when appropriate. A successful API response means Volanea accepted the message for processing; it is not identical to confirmation that the recipient opened the message or that a mailbox provider placed it in the inbox.

Why the example uses json=payload

The requests library accepts a Python dictionary through its json= parameter. It serializes the dictionary into JSON and sends it as the request body. This is safer and clearer than building JSON manually as a string.

The code also sets Content-Type: application/json explicitly. This tells the API how to parse the body. Although requests supplies the header when using json=, keeping it visible in the example makes the request contract easy to inspect and helps when porting the call to another client.

Do not replace json=payload with data=payload unless you intentionally want form-encoded data. A REST endpoint expecting JSON can reject form data or interpret it incorrectly. If you use data=, you would need to serialize the JSON yourself and preserve the correct content type.

Why the example includes both HTML and text

The html field provides the formatted version recipients see in most mail clients. The text field provides a readable fallback for plain-text clients, restrictive security settings, and recipients who prefer text-only email.

Plain text is not merely a legacy fallback. It makes operational messages easier to read in terminals and accessibility tools, and it gives recipients a usable version when HTML cannot be rendered. Keep the two versions semantically aligned: the text version should communicate the same action, destination, and important details as the HTML version.

Understand the request payload

The send request contains four core message fields:

{
  "from": "Acme Support <support@your-verified-domain.example>",
  "to": ["you@example.com"],
  "subject": "Welcome to Acme",
  "html": "<h1>Welcome to Acme</h1>",
  "text": "Welcome to Acme"
}

from identifies the sender. Use a consistent sender identity for each message class where possible. For example, account security notices may come from security@, receipts from receipts@, and product support messages from support@. Consistency helps recipients recognize the source and helps your team identify the purpose of a message during investigations.

to is an array, even when the example sends to only one address. Keeping recipients in an array gives the request an unambiguous structure and avoids changing your integration shape when a message needs more than one direct recipient. For transactional email, however, be careful about multiple recipients: sending account-specific content to a shared recipient list can expose private information.

subject should explain the event without revealing sensitive information. “Reset your password” is generally safer than placing a password, verification code, full account number, or personal data in the subject line. Subjects may be visible in notifications, lock screens, forwarding rules, and mailbox search results.

html and text are message bodies. Generate them on the server from trusted application data. If customer-supplied content appears in HTML, escape or sanitize it before interpolation. Email HTML is not a safe place to render raw user input.

Use the send call in an application

The standalone script proves that credentials, sender configuration, network access, and request shape are working. In an application, move the call into a small service function rather than duplicating the HTTP request throughout route handlers, jobs, and business-logic modules.

A focused function creates one place to manage timeouts, logging, retry policy, and payload validation. It also makes testing easier because your application can mock a single send boundary rather than every external request.

from typing import Any

import requests

VOLANEA_EMAILS_URL = "https://api.volanea.com/v1/emails"


def send_transactional_email(
    *,
    api_key: str,
    from_address: str,
    recipient: str,
    subject: str,
    html: str,
    text: str,
) -> dict[str, Any]:
    response = requests.post(
        VOLANEA_EMAILS_URL,
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
            "Accept": "application/json",
        },
        json={
            "from": from_address,
            "to": [recipient],
            "subject": subject,
            "html": html,
            "text": text,
        },
        timeout=(3.05, 15),
    )
    response.raise_for_status()
    return response.json()

Call this function after the business event has been recorded successfully. For example, when sending a receipt, persist the completed payment or order state first, then request the email send. That ordering prevents a confusing situation where a receipt is sent for an operation that later rolls back.

For important messages, do not rely solely on an in-memory request inside a web handler. A durable job or outbox pattern is generally safer. Write an “email needs sending” record as part of the same database transaction as the user action, then let a worker send it and record the result. This approach improves resilience when an HTTP request times out, a worker restarts, or a deployment interrupts a request.

Timeouts, retries, and duplicate sends

The example includes a connection timeout and a read timeout. Without a timeout, a stalled network connection can block a Python worker for an unpredictable period. That can exhaust web-server threads or job-worker capacity under load.

A timeout does not always mean Volanea did not receive the request. The provider may have accepted the message just before the network response was lost. Retrying blindly can therefore create duplicate emails.

Use this sequence for production transactional sends:

  1. Assign an internal event identifier, such as an order ID plus an event type.
  2. Store the event and its send state durably before sending.
  3. Attempt the HTTP request with a bounded timeout.
  4. Retry only failures that are plausibly transient, such as connection failures, timeouts, or server errors.
  5. Ensure the retry worker recognizes that the underlying business event has already been processed.
  6. Record the API result and later reconcile it with delivery events where applicable.

Do not retry every 4xx response. A 400 response often indicates a payload problem, such as a missing required field or invalid address format. A 401 or 403 response generally points to credentials or authorization. Retrying those responses without changing configuration only adds noise and may obscure the real issue.

For 429 responses, slow down according to the response information and your application’s queueing policy. For 5xx responses, retry with exponential backoff and a cap. The important rule is that retries should be aware of the message’s business identity, not merely the HTTP status code.

Test safely before production

Start with an inbox you control. Confirm that the message appears, that its sender is correct, and that the plain-text fallback reads well. Then test with a small set of real mailbox providers your recipients commonly use.

A practical test checklist includes:

  • Confirm the script fails clearly when VOLANEA_API_KEY is absent.
  • Confirm the sender identity is accepted by your sending configuration.
  • Check the email on desktop and mobile mail clients.
  • Check the plain-text view, not only the formatted HTML view.
  • Verify that links use HTTPS and point to the correct environment.
  • Send a deliberately malformed request in a non-production environment to verify error logging.
  • Test a temporary network failure to make sure your application does not create uncontrolled retries.
  • Confirm monitoring distinguishes API acceptance from downstream delivery or engagement events.

Avoid using a production customer address for first tests unless you have a clear operational reason and the recipient expects the message. A test inbox gives you freedom to revise sender names, HTML, and copy without accidentally confusing a customer.

Common errors

401 Unauthorized or authentication failures

A 401 response usually means the API key is missing, malformed, revoked, or not being sent as expected. First confirm that the environment variable is present in the process that starts Python:

python -c "import os; print(bool(os.getenv('VOLANEA_API_KEY')))"

If this prints False, Python cannot see the variable. Check whether .env is in the working directory, whether load_dotenv() runs before os.getenv(), and whether your deployment platform has the secret configured for the correct service and environment.

Also inspect the authorization header construction. It must include the authentication scheme and a space before the key:

"Authorization": f"Bearer {api_key}"

Do not log the full authorization header or API key while debugging. If you suspect a key was exposed, rotate it and update the deployed secret rather than continuing to use it.

400 Bad Request or validation errors

A 400 response means the service understood the HTTP request but rejected its contents. Print the response body during development because it may describe the invalid field:

print(response.status_code)
print(response.text)

Common payload mistakes include an empty subject, invalid recipient address, malformed sender identity, missing HTML or text content, or passing to as a string when the API expects an array. In the guide’s example, to is deliberately written as [to_address].

Do not swallow a 400 and report “email sent” to your application. Make the failure observable and preserve a safe error summary so a developer can repair the message generator.

415 Unsupported Media Type or wrong content type

This error commonly appears when a JSON API receives form data, multipart data, or a body that lacks the expected media type. Send a Python dictionary with json=payload and use this header:

"Content-Type": "application/json"

Avoid this pattern for a JSON request:

requests.post(API_URL, data=payload)

With data=payload, requests encodes the dictionary as form data by default. The endpoint may not parse it as JSON. Use json=payload unless you have a documented reason to send another format.

403 Forbidden or sender authorization errors

A 403 can indicate that the key is valid but lacks permission for the requested operation or that the sender identity is not permitted. Check that you are using the correct environment’s credentials and that the address in VOLANEA_FROM belongs to a configured sending domain.

Do not solve a sender problem by substituting a consumer mailbox address, such as a personal Gmail or Outlook address, into the from field. Use an address on a domain your organization controls and has prepared for sending.

429 Too Many Requests

A 429 response means your application is sending faster than the service currently allows. The right response is to queue work, reduce concurrency, and retry after a controlled delay. Do not create a tight loop around requests.post().

For bursty event streams, put messages in a queue and have a worker consume them at a known rate. This gives you control over ordering, retries, and backpressure. It also prevents a sudden traffic spike from turning into a pile of simultaneous HTTP retries.

Timeouts and connection failures

A requests.exceptions.Timeout can occur during connection establishment or while waiting for a response. A ConnectionError can result from DNS resolution issues, outbound firewall rules, proxy configuration, or a temporary network failure.

The sample handles both errors and exits with a non-zero code. In a web application, translate these exceptions into a retryable job state rather than showing raw infrastructure errors to end users. Remember that a timeout leaves the send result uncertain; design retry behavior with duplicate prevention in mind.

Async and await mistakes

The code in this guide uses requests, which is synchronous. Do not write this:

await requests.post(API_URL, headers=headers, json=payload)

requests.post() returns a normal response object, not an awaitable coroutine. Python will raise an error if you try to await it.

If your application is asynchronous, such as an ASGI service, either run a synchronous client safely outside the event loop or use an async-capable HTTP library and follow that library’s documented request syntax. Do not mix sync and async styles casually: calling blocking requests.post() directly inside a high-concurrency async endpoint can block the event loop and degrade unrelated requests.

ModuleNotFoundError: No module named 'requests'

This means the Python interpreter running the script cannot find the installed package. Activate the intended virtual environment and install packages through that interpreter:

python -m pip install requests python-dotenv
python send_email.py

If the issue continues, compare which python with which pip on macOS or Linux, or use python -m pip exclusively. On Windows, verify that the active virtual environment is the one from which the script is being executed.

Deliverability considerations for transactional email

A successful API call is only the start of reliable email delivery. Transactional messages should have a clear purpose, expected timing, and trustworthy sender identity.

Use stable sender domains and recognizable display names. A password-reset request should arrive promptly and identify the product that initiated it. A receipt should contain the purchase information a customer expects. A sudden change in sender identity, inconsistent branding, or a message that arrives long after the triggering action can make even legitimate email look suspicious.

Keep transactional content focused. A receipt can include an appropriate account-management link, but it should not become a broad promotional newsletter. Separating operational and marketing intent improves customer expectations and makes it easier for your application to apply the right consent and suppression rules.

Make the email useful without requiring images or complex CSS to load. Put essential information in text and HTML, use descriptive links, and ensure the message remains understandable when images are disabled. For security-sensitive messages, avoid including secrets directly in the body or subject; use time-limited, single-purpose links where your product design supports them.

Next steps

Once your first Python send is working, expand the integration in two directions.

First, add webhooks. Webhooks allow your application to receive event notifications after a message is accepted, delivered, bounced, complained about, or otherwise processed. Build a small HTTPS endpoint that validates incoming webhook requests, records events idempotently, and updates your application’s delivery state. Do not assume one webhook event will arrive only once or in perfect chronological order; event handlers should tolerate retries and duplicates.

Second, introduce templates. Templates let your team keep a consistent structure for recurring messages such as receipts, invitation emails, and account notifications. Whether you render HTML in Python or use provider-side template capabilities available in your account, keep variables explicit, validate required fields, escape untrusted values, and maintain a text alternative. A template system should reduce repeated markup without making message behavior opaque.

As your volume grows, add a durable outbox or queue, structured logs with safe request identifiers, alerting for elevated failures, and tests for your most important email flows. These controls turn a working send call into dependable messaging infrastructure.

FAQ

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

No. A standard HTTPS client is sufficient for a REST integration. This guide uses requests to send a JSON payload with a bearer API key, so the code does not depend on a provider-specific Python SDK method.

Should I put my Volanea API key in the Python source file?

No. Load it from an environment variable or a deployment secret manager. Keep .env files out of version control and never expose the key in frontend code, screenshots, logs, or support tickets.

Does a successful API response mean the recipient received the message?

It means the service accepted the request for processing. Delivery, bounces, complaints, and engagement are later outcomes. Use webhook events and application monitoring to track those downstream states.

Why should I include a plain-text email body?

Plain text provides a readable fallback for clients or settings that do not render HTML. It also improves accessibility and ensures the essential information remains available without formatting.

Can I use this code in an async Python application?

The example is synchronous because it uses requests. For an async application, do not await requests.post(). Use an async HTTP client according to its documentation or isolate the synchronous request from the event loop.