> ## 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.

# Send Email from Azure Functions (Node.js)

> Send email from Azure Functions in Node.js with the AhaSend SDK, a protected HTTP handler, local Core Tools tests and sandbox-safe configuration.

AhaSend lets you send transactional email from Azure Functions in Node.js with the official SDK; this guide builds a protected HTTP function and runs it locally.

## 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 Node.js 22 and [Azure Functions Core Tools v4](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local). The code uses the [Node.js v4 programming model](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node).

## Install the Dependencies

Run these commands in a new directory:

```bash theme={null}
npm init -y
npm install --save-exact @ahasend/sdk@0.2.1 @azure/functions@4
npm pkg set main=index.cjs
```

## Try a Sandbox Send

Run this short SDK check locally before adding the longer server integration below. Use a [verified sender](/docs/domains), a [send-only API key](/docs/send-api/credentials), and your account ID. It sends to a reserved test address with `sandbox: true`, so no email is delivered.

In the server project directory, add `.env` to `.gitignore`, then create `.env` with `AHASEND_API_KEY`, `AHASEND_ACCOUNT_ID`, and `AHASEND_FROM` (an address on your verified domain). Keep these values out of browser code and AI prompts. Use Node.js 22 or newer. Save this as `quick-send.mjs`:

```js quick-send.mjs theme={null}
import { AhaSendClient } from "@ahasend/sdk";
try {
  const client = AhaSendClient.fromEnv();
  const result = await client.messages.send({
    from: { email: process.env.AHASEND_FROM },
    recipients: [{ email: "recipient@example.com" }],
    subject: "My first sandbox email",
    text_content: "Hello from AhaSend",
    sandbox: true,
  });
  const statuses = result.data.map((entry) => entry.status);
  console.log({ statuses });
  if (!statuses.length || statuses.some((s) => !["queued", "scheduled"].includes(s)))
    throw new Error("A recipient was rejected");
} catch {
  console.error("Sandbox send failed; check your credentials, sender and account.");
  process.exitCode = 1;
}
```

Run `node --env-file=.env quick-send.mjs` from that directory. The output lists each recipient status; `queued` or `scheduled` means accepted for sandbox processing. The process exits with a failure if the request or any recipient fails. Continue below for the full integration.

## Add the Shared Sending Code

Save this as `send-email.cjs` beside the function file. Configure `AHASEND_API_KEY`, `AHASEND_ACCOUNT_ID`, `AHASEND_FROM`, `AHASEND_TO` and a long random `SEND_TOKEN` on the server. Set `AHASEND_TO=recipient@example.com` and `AHASEND_DELIVERY_MODE=sandbox` for local testing. Keep a sandbox credential until you are ready for real delivery; changing the mode to `live` alone cannot override a sandbox credential.

```js send-email.cjs theme={null}
const { createHash, timingSafeEqual } = require("node:crypto");
const { AhaSendClient } = require("@ahasend/sdk");
const digest = (value) => createHash("sha256").update(value).digest();
let client;
exports.sendEmail = async (body, authorization = "") => {
  const token = process.env.SEND_TOKEN;
  if (!token) return [500, { error: "Missing server configuration" }];
  if (!timingSafeEqual(digest(authorization), digest(`Bearer ${token}`)))
    return [401, { error: "Unauthorized" }];
  if (!body || typeof body.event_id !== "string" ||
      !/^[A-Za-z0-9_-]{8,64}$/.test(body.event_id))
    return [400, { error: "event_id must be 8-64 letters, digits, _ or -" }];
  const mode = process.env.AHASEND_DELIVERY_MODE || "sandbox";
  if (!["sandbox", "live"].includes(mode) ||
      !["AHASEND_API_KEY", "AHASEND_ACCOUNT_ID", "AHASEND_FROM", "AHASEND_TO"]
        .every((name) => process.env[name]))
    return [500, { error: "Missing or invalid server configuration" }];
  try {
    client ||= AhaSendClient.fromEnv();
    const result = await client.messages.send({
      from: { email: process.env.AHASEND_FROM },
      recipients: [{ email: process.env.AHASEND_TO }],
      subject: "Welcome", text_content: "Your account is ready.",
      sandbox: mode === "sandbox",
    }, { idempotencyKey: `${mode}-welcome-${body.event_id}` });
    const statuses = result.data.map((item) => item.status);
    if (!statuses.length || statuses.some((s) => !["queued", "scheduled"].includes(s)))
      return [422, { error: "A recipient was rejected" }];
    return [202, { statuses }];
  } catch {
    return [502, { error: "Email request failed" }];
  }
};
```

The caller supplies only the business event ID. For a real signup, read the recipient from your database after checking the caller's access. Save the exact message with that event ID and reuse both when retrying. Read the [idempotency limits](/docs/api-reference/idempotency) before retrying an uncertain send. Add request limits at the hosting layer and log an internal event ID instead of credentials or message bodies.

## Create the HTTP Function

Save this as `index.cjs`. The function accepts POST requests and also checks your server's bearer token; Azure's function key is an additional hosted check.

```js index.cjs theme={null}
const { app } = require("@azure/functions");
const { sendEmail } = require("./send-email.cjs");
app.http("sendEmail", {
  methods: ["POST"], authLevel: "function",
  handler: async (request) => {
    const raw = await request.text();
    if (Buffer.byteLength(raw) > 16384) return { status: 413 };
    let body;
    try { body = JSON.parse(raw); }
    catch { return { status: 400, jsonBody: { error: "Invalid JSON" } }; }
    const [status, jsonBody] = await sendEmail(body, request.headers.get("authorization") || "");
    return { status, jsonBody };
  },
});
```

Create `host.json`:

```json host.json theme={null}
{"version":"2.0"}
```

## Run with Core Tools

Keep `.env` and `local.settings.json` out of Git. Export the variables from the shared code, then start the local host:

```bash theme={null}
FUNCTIONS_WORKER_RUNTIME=node func start
```

In another shell with `SEND_TOKEN` set, invoke the local function:

```bash theme={null}
curl --fail-with-body http://localhost:7071/api/sendEmail   -H "Authorization: Bearer $SEND_TOKEN" -H 'Content-Type: application/json'   -d '{"event_id":"local-azure-node-001"}'
```

Expect HTTP 202 with acceptance statuses. Missing or wrong bearer tokens return 401; invalid event IDs return 400. Local Core Tools does not enforce function keys, so the handler keeps its own token check. To repeat a new test use a new event ID; to retry the same job keep its ID and message unchanged.

## Prepare the Hosted App

Select Node.js 22 and Functions runtime v4. Put the values in app settings, with [Key Vault references](https://learn.microsoft.com/en-us/azure/app-service/app-service-key-vault-references) for secrets. Send the Azure function key as `x-functions-key` in addition to the bearer token. Use your application's identity checks and request limits before allowing callers to trigger mail. Follow [Microsoft's deployment steps](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local) to publish the app.

Track final delivery with [webhooks](/docs/integrations/webhooks). Also see [Azure in Python](/docs/guides/azure-functions-python), [Azure in Go](/docs/guides/azure-functions) and [Node.js SMTP](/docs/smtp/nodejs).

## 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).
