Skip to main content
This guide shows how to use AhaSend to send transactional email from Azure Functions in Go, keep credentials on the server, and test a sandbox send. This guide runs Go as an Azure Functions custom handler: a small HTTP server the Functions host forwards requests to. Ordinary net/http code works, including the SDK’s verifier.ParseRequest. As of September 2026, Microsoft documents first-class Go support as a public preview and recommends it for new Go function apps. Check the Azure custom-handler documentation and its linked Go preview requirements before choosing a hosting plan. Use a send-only key from Credentials → Add → API Key v2 with messages:send:{your-domain}. Use messages:send:all when sending from many domains. For management tasks such as creating a webhook, use a separate full key from Account Settings → API Keys with the needed scopes. See send-only credentials and authentication. Keep credentials on the server. Use sandbox mode for tests. Reuse a stable, business-event idempotency key only with the exact same payload; use different keys for sandbox and live sends. Check every recipient’s status and return a failure when any item is error; HTTP 202 alone does not mean every recipient was accepted. For webhooks, verify the raw request bytes using the full secret as the raw UTF-8 HMAC key, including its aha-whsec- or aha-rsec- prefix. Never base64-decode the secret. The AhaSend SDK verifiers handle this. These examples use a 30,000,000-byte webhook body limit; a hosting platform may impose a smaller limit. Acknowledge verified unknown event types with 2xx. See verification and retry policy.

Prerequisites

  • Go installed
  • An AhaSend account with a verified sending domain
  • An API key with the domain-scoped messages:send:{your-domain} scope matching the domain in From.Email (use messages:send:all only if the key must cover several domains), and your account ID
  • A Linux/x64 Azure Function App configured for a custom handler (select .NET as the runtime stack when creating it), the Azure Functions Core Tools (func), and the Azure CLI (az)

Project Layout

A custom handler app pairs one Go binary with the standard Functions metadata files, one folder per function:

Install the SDK

Try a Sandbox Send

Run this short SDK check locally before adding the longer server integration below. Use a verified sender, a send-only API key, and your account ID. It sends to a reserved test address with sandbox: true, so no email is delivered. In your terminal, set AHASEND_API_KEY, AHASEND_ACCOUNT_ID, and AHASEND_FROM (an address on your verified domain) as environment variables. Go does not load .env files by itself. Keep the key in your local secret store and out of source control. Save this standalone check as quick-send.go in the module directory where you ran go get:
quick-send.go
Run go run quick-send.go. It prints the HTTP status and each recipient status and exits with a failure on a request error or rejected recipient. This checks the SDK locally; it does not deploy a cloud function. Delete quick-send.go after the check so its main function does not conflict with the full app below. google/uuid arrives with the SDK as a transitive dependency, but the account ID is a uuid.UUID that your own code parses, so it needs its own go get or the build fails on a missing go.sum entry.

Configure the Functions Host

1

host.json

Point the host at your compiled binary and enable raw HTTP pass-through:
host.json
enableProxyingHttpRequest: true is what makes this guide work. Without it, the host wraps every request in a JSON envelope instead of passing the raw HTTP request through: plain JSON decoding and webhook signature verification both break. Older custom-handler samples use enableForwardingHttpRequest for the same purpose; the host still honours it, but enableProxyingHttpRequest is the setting Microsoft documents today.
2

One function.json per function

Each folder name becomes the function name and its default route (/api/SendWelcome, /api/AhaSendWebhook):
SendWelcome/function.json
AhaSendWebhook/function.json
The webhook endpoint is anonymous because the HMAC signature, not a function key, is what authenticates a delivery — the verifier rejects anything it can’t attribute to your secret. You can add a key on top by switching this function to authLevel: "function" and registering the webhook URL with ?code=<function-key> appended, but then rotating that key silently breaks deliveries, and AhaSend disables a webhook after more than 100 consecutive failed attempts.

Configure App Settings

Azure Function App settings are exposed to your binary as environment variables. Set them in the Portal under Function App → Environment variables, or with the CLI:
For local development, put the same values in local.settings.json:
local.settings.json
local.settings.json contains secrets, so add it to .gitignore.

Write the Azure Functions Handler

The binary is an ordinary net/http server. The only Azure-specific detail is the listen port: the host tells you where to listen via FUNCTIONS_CUSTOMHANDLER_PORT. Create the SDK client and webhook verifier once at startup and reuse them across invocations.
main.go
This function mails whatever address its caller supplies, so the function key is the only thing standing between it and an open relay. Keep authLevel: "function", treat the key like a password, and rotate it if it leaks. In production, prefer looking the recipient up in your own datastore from an internal identifier over accepting an address on the wire — Azure Functions applies no per-caller rate limit of its own, so a leaked key is a spam faucet until you revoke it.
The caller owns the idempotency key, which is what makes a retry safe. Reusing a key with the same body inside 24 hours replays the stored result instead of sending again; reusing it with a different body returns 422, so derive the key from the thing you’re sending (welcome-<user-id>, say) rather than from the attempt. Left to itself the SDK generates a fresh random key per call, which covers only its own internal retries — of 429, 5xx, and network failures, with exponential backoff — and does nothing about a caller that retries. Note that a 5xx outcome is never stored, so a retry after a server error can still send twice.

Handle Webhooks

Because the host passes the original HTTP request through untouched, the SDK’s ParseRequest works directly on it: it verifies the HMAC signature over the raw body bytes, rejects timestamps outside its five-minute tolerance in either direction, and returns a typed event:
webhook.go
Create the webhook in your AhaSend dashboard pointing at https://your-function-app.azurewebsites.net/api/AhaSendWebhook, and copy its secret into the AHASEND_WEBHOOK_SECRET app setting exactly as shown (including the aha-whsec- prefix). Timestamp checking is not complete replay protection: the same correctly signed delivery can be replayed inside the accepted window. Before acknowledging the webhook, atomically commit the webhook-id request header together with a durable work/outbox record, and ignore IDs already committed. Don’t commit the ID separately: if the process fails before recording the work, a retry would look like a duplicate and the event would be lost. Return 200 promptly because AhaSend retries webhooks that don’t get a timely 2xx. For slow work, durably enqueue it before returning and process it with a queue-triggered function — which needs an extensionBundle entry in host.json, since custom handlers get non-HTTP triggers through extension bundles. Don’t start a background goroutine and return: Azure Functions can shut down the instance after the invocation completes.

Build and Deploy

The Linux/x64 Function App used in this guide needs a linux/amd64 binary, so cross-compile, then publish with Core Tools:
CGO_ENABLED=0 produces a static binary, so the handler doesn’t depend on the C libraries that happen to be on your build machine. For local testing, build for your own OS and run func start: the host reads local.settings.json and proxies to your binary. In CI, run the same two commands after az login (or use a deployment workflow such as the official Azure Functions GitHub Action).

Going Further

For substitutions, attachments and scheduled sends, use the sending guide. The Node.js SDK and Go SDK references explain client options, per-recipient results and retries. Use idempotency rules when your application retries a business operation.

Troubleshooting

Check that enableProxyingHttpRequest is true in host.json. Without it the host sends your binary a JSON invocation envelope instead of the raw request, so the body your code sees is not what AhaSend signed and ParseRequest (and plain json.Decode) fail.
Verify defaultExecutablePath matches the binary name you deployed, that the binary was built with CGO_ENABLED=0 GOOS=linux GOARCH=amd64 for a Linux plan, and that it listens on FUNCTIONS_CUSTOMHANDLER_PORT. Hardcoding a port makes the host time out waiting for the worker.
The API key is missing, malformed, or revoked. Confirm the AHASEND_API_KEY app setting is present in the Function App environment (Portal → Environment variables). local.settings.json values are not deployed to Azure.
SendWelcome uses authLevel: "function", so requests need a function key: https://your-function-app.azurewebsites.net/api/SendWelcome?code=<function-key>.
The From address must belong to a verified sending domain on your account. Check domain status in the dashboard.
Other language guides: Azure Functions with Node.js and Azure Functions with Python.