> ## Documentation Index
> Fetch the complete documentation index at: https://ahasend.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> AhaSend stores all message data in the EU. See /facts.
> The API base URL is https://api.ahasend.com. Send the API key in the Authorization: Bearer header. See /api-reference/authentication.
> Use send.ahasend.com for SMTP in the EU, or send-us.ahasend.com as a US connection point forwarding to the EU. Ports 25, 587 and 2525 use STARTTLS. Port 465 is not supported. See /smtp.
> For send-only integrations, create an API Key v2 under Credentials → Add and scope it to messages:send:{your-domain}. See /send-api/credentials and /api-reference/scopes.

# How to Send Email from AWS Lambda (Python)

> Send email from AWS Lambda in Python with AhaSend: a small HTTP API example, an IAM-invoked handler, sandbox tests and safe request retries.

AhaSend lets you send transactional email from AWS Lambda in Python through the HTTP API; this guide adds a locally testable handler for trusted, IAM-authorized invocations.

## Prerequisites

Use a [verified sending domain](/docs/domains), your account ID, and a domain-scoped [send-only API v2 key](/docs/send-api/credentials). Use [sandbox mode](/docs/send-api/sandbox) for every test. Management tasks need a separate [full API key](/docs/api-reference/authentication).

Use Python 3.13, a runtime supported by [AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/lambda-python.html). This example needs no third-party packages.

## Prepare the Python Environment

```bash theme={null}
python3.13 -m venv .venv
source .venv/bin/activate
```

## Try a Sandbox Send

Set `AHASEND_API_KEY`, `AHASEND_ACCOUNT_ID` and `AHASEND_FROM` in your shell or local secret store. Use a [send-only key](/docs/send-api/credentials) and an address on your [verified domain](/docs/domains). Save this as `quick_send.py`:

```python quick_send.py theme={null}
import json, os, urllib.request
payload = {
    "from": {"email": os.environ["AHASEND_FROM"]},
    "recipients": [{"email": "recipient@example.com"}],
    "subject": "My first sandbox email",
    "text_content": "Hello from AhaSend",
    "sandbox": True,
}
url = f"https://api.ahasend.com/v2/accounts/{os.environ['AHASEND_ACCOUNT_ID']}/messages"
request = urllib.request.Request(url, json.dumps(payload).encode(), headers={
    "Authorization": f"Bearer {os.environ['AHASEND_API_KEY']}",
    "Content-Type": "application/json",
})
with urllib.request.urlopen(request, timeout=10) as response:
    statuses = [item["status"] for item in json.load(response)["data"]]
if not statuses or any(s not in ("queued", "scheduled") for s in statuses):
    raise RuntimeError("A recipient was rejected")
print({"statuses": statuses})
```

Run `python quick_send.py` in the activated environment. It prints acceptance statuses and exits with an error if sending fails. Sandbox acceptance does not deliver mail. The production version below adds input checks and a stable request key.

## Add the Shared Sending Code

Save this as `send_email.py` beside the function file. It uses Python's standard library, so this example needs no SDK package. Set `AHASEND_API_KEY`, `AHASEND_ACCOUNT_ID`, `AHASEND_FROM`, `AHASEND_TO` and `SEND_TOKEN` in the runtime environment. Use a long random `SEND_TOKEN` for requests from your trusted server. For the first run set `AHASEND_TO=recipient@example.com` and `AHASEND_DELIVERY_MODE=sandbox`; keep a sandbox credential in place while testing. Set `live` only when ready to deliver to your own approved recipients.

```python send_email.py theme={null}
import hmac, json, os, re, urllib.error, urllib.request

def send_email(body, authorization):
    token = os.environ.get("SEND_TOKEN", "")
    if not token:
        return 500, {"error": "Missing server configuration"}
    if not hmac.compare_digest(authorization.encode(), ("Bearer " + token).encode()):
        return 401, {"error": "Unauthorized"}
    event_id = body.get("event_id") if isinstance(body, dict) else None
    if not isinstance(event_id, str) or not re.fullmatch(r"[A-Za-z0-9_-]{8,64}", event_id):
        return 400, {"error": "event_id must be 8-64 letters, digits, _ or -"}
    mode = os.environ.get("AHASEND_DELIVERY_MODE", "sandbox")
    required = ("AHASEND_API_KEY", "AHASEND_ACCOUNT_ID", "AHASEND_FROM", "AHASEND_TO")
    if mode not in ("sandbox", "live") or any(not os.environ.get(k) for k in required):
        return 500, {"error": "Missing or invalid server configuration"}
    payload = {
        "from": {"email": os.environ["AHASEND_FROM"]},
        "recipients": [{"email": os.environ["AHASEND_TO"]}],
        "subject": "Welcome",
        "text_content": "Your account is ready.",
        "sandbox": mode == "sandbox",
    }
    url = f"https://api.ahasend.com/v2/accounts/{os.environ['AHASEND_ACCOUNT_ID']}/messages"
    request = urllib.request.Request(url, json.dumps(payload).encode(), headers={
        "Authorization": f"Bearer {os.environ['AHASEND_API_KEY']}",
        "Content-Type": "application/json",
        "Idempotency-Key": f"{mode}-welcome-{event_id}",
    })
    try:
        with urllib.request.urlopen(request, timeout=10) as response:
            result = json.load(response)
        statuses = [item["status"] for item in result["data"]]
        if not statuses or any(s not in ("queued", "scheduled") for s in statuses):
            return 422, {"error": "A recipient was rejected"}
        return 202, {"statuses": statuses}
    except (urllib.error.URLError, TimeoutError, ValueError, KeyError, TypeError):
        return 502, {"error": "Email request failed"}
```

Only your server chooses the sender, recipient and message body. Look up a user's verified address from your database before adapting this to a real signup flow. Keep that saved message unchanged when retrying an `event_id`; a new business event needs a new ID. See [idempotency](/docs/api-reference/idempotency) for time limits and uncertain outcomes. Record an internal event ID on failures without logging API keys or message bodies. Add request limits at the hosting layer.

## Create the Lambda Handler

Save the following file as `lambda_function.py`. This handler accepts a direct Lambda invocation, with a JSON object containing `event_id`. Let IAM decide which backend roles may invoke it. If you later add a Function URL, use AWS\_IAM authentication and adapt the request parsing; this file does not parse HTTP event envelopes.

```python lambda_function.py theme={null}
import os
from send_email import send_email

def lambda_handler(event, context):
    status, result = send_email(event, "Bearer " + os.environ["SEND_TOKEN"])
    if status != 202:
        raise RuntimeError(f"Email job failed with status {status}")
    return result
```

## Run It Locally

Export the environment variables named above, then run this in the same directory. A new test run needs a new `event_id`; retry a previous job with its original ID.

```bash theme={null}
python -c 'from lambda_function import lambda_handler; print(lambda_handler({"event_id":"local-lambda-001"}, None))'
```

Expect `queued` or `scheduled` statuses. An invalid event or API failure raises an exception, so the caller can record the failed job. Before using asynchronous Lambda invocation, account for AWS retries and the AhaSend idempotency time limit; keep a durable job record.

## Configure the Hosted Function

Package both Python files at the archive root and set the handler to `lambda_function.lambda_handler` with runtime `python3.13`. Give the function enough time for its 10-second HTTP request and startup. Provide outbound HTTPS access; a function inside a private VPC needs an appropriate network path to the public API. Keep secrets in AWS Secrets Manager and load them in your deployment's secret-handling code. Follow [AWS's Python packaging steps](https://docs.aws.amazon.com/lambda/latest/dg/python-package.html) when publishing.

Use [delivery webhooks](/docs/integrations/webhooks) to track the final outcome after acceptance. See [Lambda in Node.js](/docs/guides/aws-lambda) for the SDK version and [Python SMTP](/docs/smtp/python) for the SMTP alternative.

## Related Guides

* Before sending: [verify a domain](/docs/domains) and [create a send-only key](/docs/send-api/credentials).
* Other ways to send: [REST API](/docs/send-api/send-email), [SMTP](/docs/smtp), [CLI quickstart](/docs/cli/quickstart), [Node.js SDK](/docs/guides/nodejs-sdk) and [Go SDK](/docs/guides/go-sdk).
* Request rules: [API authentication](/docs/api-reference/authentication), [scopes](/docs/api-reference/scopes), [idempotency](/docs/api-reference/idempotency), [errors](/docs/api-reference/errors) and [rate limits](/docs/api-reference/rate-limits).
* Testing and events: [sandbox mode](/docs/send-api/sandbox), [CLI webhook testing](/docs/cli/webhook-testing), [event payloads](/docs/api-reference/webhooks), [signature verification](/docs/api-reference/webhooks/security) and [delivery retries](/docs/api-reference/webhooks/retry-policy).
* Data and limits: [retention](/docs/retention), [tracking](/docs/tracking/open-tracking) and [plans and feature availability](/docs/facts#plans-and-features).
