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 inFrom.Email(usemessages:send:allonly 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 withsandbox: 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
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
2
One function.json per function
Each folder name becomes the function name and its default route (The webhook endpoint is
/api/SendWelcome, /api/AhaSendWebhook):SendWelcome/function.json
AhaSendWebhook/function.json
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: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 ordinarynet/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
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’sParseRequest 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
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
Webhook verification always fails, or JSON decoding sees a strange envelope
Webhook verification always fails, or JSON decoding sees a strange envelope
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.Functions host can't start the handler
Functions host can't start the handler
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.401 from the AhaSend API
401 from the AhaSend API
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.401 calling the send endpoint
401 calling the send endpoint
SendWelcome uses authLevel: "function", so requests need a function key: https://your-function-app.azurewebsites.net/api/SendWelcome?code=<function-key>.400 error mentioning the from address
400 error mentioning the from address
The
From address must belong to a verified sending domain on your account. Check domain status in the dashboard.Related Guides
- Before sending: verify a domain and create a send-only key.
- Other ways to send: REST API, SMTP, CLI quickstart, Node.js SDK and Go SDK.
- Request rules: API authentication, scopes, idempotency, errors and rate limits.
- Testing and events: sandbox mode, CLI webhook testing, event payloads, signature verification and delivery retries.
- Data and limits: retention, tracking and plans and feature availability.

