Skip to main content
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. Azure also has first-class Go support in public preview, which Microsoft recommends for new Go function apps — but during preview it runs only on the Flex Consumption plan, whereas custom handlers are supported across every Azure Functions hosting option.

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

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 100 consecutive failures.

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

  • Templating: set Substitutions: map[string]interface{}{"first_name": "Jane"} and use {{ first_name }} in the subject or body.
  • Batch sends: Recipients accepts up to 100 entries; each gets a separate message.
  • Tags: set Tags: []string{"welcome"} to filter messages in list and statistics queries. Webhooks are scoped by domain, not by tag, and message events do not carry the tags.
  • Batch idempotency: one key covers one whole CreateMessage call, recipient list included. Adding a recipient changes the body, so the same key then returns 422 rather than sending to the newcomer.
  • Other platforms: deploying on Google Cloud instead? See the Cloud Run functions guide; for long-running servers, start with Gin or chi.
  • Attachments: add Attachments with Data, ContentType, and FileName. Set Base64: true for binary files such as PDFs.
See the API reference for every service the SDK exposes (DomainsAPI, SuppressionsAPI, StatisticsAPI, and more).

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.