net/http handlers, so AhaSend’s webhook verifier accepts the *http.Request your handler receives. Middleware registered with Router.Use runs in registration order and must leave the signed webhook body untouched.
The examples use gorilla/mux v1.8.1. The same net/http handlers can also be mounted on http.ServeMux or another router.
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
- An AhaSend account with a verified sending domain
- An API key with the domain-specific
messages:send:{your-domain}scope, and your account ID
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 is a transitive dependency of the SDK, but the account ID is a uuid.UUID, so your own code imports it directly and it needs its own go get.
Configure Environment Variables
sandbox or live delivery mode and refuses to start for any other value.
Create the Client
Create the client once at startup and share it across handlers: it maintains its own rate-limit, retry, and idempotency state. Use anhttp.Server with explicit limits and timeouts to serve the router:
main.go
Send an Email from a gorilla/mux Handler
gorilla/mux handlers are ordinarynet/http handlers, so decode the JSON body with encoding/json:
send.go
2xx from the API is not a per-recipient guarantee: every entry in response.Data carries its own queued, scheduled, or error status, so inspect them all before reporting success. On the error path, apiErr.IsRetryable() separates the transient failures — 429, 5xx, network, and the 409 returned while an earlier request with the same key is still in flight — from terminal ones such as a validation error, a missing scope, or a key reused with a different body, which is why only the former is reported to the caller as a gateway error.
This example uses a dedicated bearer token for a server-to-server endpoint. For a user-facing route, use your application’s session authentication and authorization and load the recipient from server-authoritative storage instead of accepting an email address from the browser. Never expose either bearer token to client-side code or logs. Serve the route only over HTTPS, and apply per-caller request-rate and concurrency limits at your reverse proxy: an endpoint that sends to a caller-supplied address is a mail relay for anyone holding the token.
The explicit idempotency key protects a retry of the same business event outside the SDK’s internal retry loop. Reuse it only for the exact same payload and never log it — the API matches a key against the request body, so the same key with a changed body is rejected with 422 rather than replayed, which is why the key carries the delivery mode. Stored outcomes replay for 24 hours, but a 5xx is not stored and the key is released, so a retry after a server error can still send twice; reconcile an uncertain result before issuing another send when duplicates are unacceptable.
Handle Webhooks
gorilla/mux hands your handler the*http.Request, so the SDK’s ParseRequest can verify the HMAC signature over the exact raw body and return a typed event. Timestamp validation rejects stale signatures but does not deduplicate a valid delivery replayed inside the tolerance window.
webhook.go
main.go registers the route before starting the server. The included memory store is only for local testing: it loses data on restart, grows without cleanup, and cannot coordinate replicas. Replace it with a durable store before adding side effects. Startup stops if registration fails.
Create the webhook in your AhaSend dashboard pointing at https://your-app.com/webhooks/ahasend, and copy its secret into AHASEND_WEBHOOK_SECRET exactly as shown (including the aha-whsec- prefix).
maxWebhookBodyBytes bounds one request, not the process, and it is sized for inbound message.routing deliveries, which embed the received email’s attachments. Lower it if this endpoint only receives message, suppression, and domain events — the body is buffered and copied again to build the signed string before the signature is checked, so the cap sets how much memory an unauthenticated caller can make each in-flight request hold.
EnqueueOnce must atomically store the verified webhook-id and durable work/outbox record, retaining the ID for at least the delivery and retry horizon. A duplicate receives 2xx without running the work again, while a storage failure receives 5xx so it can be retried. Process queued work idempotently outside the request; an untracked goroutine can be lost when the process exits. Apply request-rate and concurrency limits at the reverse proxy, and never log the raw body, signature, whole event/error, subjects, addresses, or message content.
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
401 or 403 from the API
401 or 403 from the API
A
401 means the API key is missing, malformed, or revoked; a 403 means the key is valid but does not carry the sending domain’s messages:send scope. Both reach the same handler branch. Verify AHASEND_API_KEY is set in the process environment and review the API credentials guide without printing the key.Webhook verification always fails
Webhook verification always fails
Something consumed the request body before
ParseRequest ran. Check r.Use(...) middleware for body readers. Also confirm the secret matches the dashboard exactly, including the aha-whsec- prefix.405 Method Not Allowed on my routes
405 Method Not Allowed on my routes
.Methods(http.MethodPost) restricts the route to POST only. A request that matches the path with any other method is answered by gorilla/mux’s built-in 405 handler, which runs without your Router.Use middleware. Make sure your test request uses POST.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.

