Skip to main content
The AhaSend Go SDK lets you send email, handle recipient results and verify webhooks; this guide covers installation and the client options. This page is the SDK reference. To wire it into a specific framework, pick a guide from the sidebar. Before sending, verify a sending domain and create an API key plus an account ID. Give the key the domain-specific messages:send:{your-domain} scope when it only sends from one domain, and keep it in a server-side secret store. 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.

Install

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.

Send Your First Email

main.go
Run it with go run main.go. The example sets Sandbox to true, so nothing is delivered even though the message still runs through validation and fires the corresponding webhooks. Replace the example’s idempotency key with a stable, server-derived identifier for the actual business operation, and do not carry a key from a sandbox send over to the live one: flipping Sandbox changes the request body, and the API rejects a reused key whose payload differs with 422 instead of sending. See Sandbox Mode. Note the loop. Recipients takes up to 100 addresses and each one becomes its own message, so response.Data holds one entry per recipient. Reading Data[0] and calling it done would hide the rest. Optional fields are pointers, so the SDK exports helpers such as ahasend.String() to build them inline. Beyond the fields above: Tags []string, Substitutions map[string]interface{} for {{ variable }} templating, plus attachments, scheduling, tracking, and retention.

Configure the Client

The following configuration fragment belongs inside your startup function and requires import "time". Build the client once at startup and share it. It holds rate-limit, retry, and idempotency state. api.NewAPIClientFromEnv() builds the same client from the AHASEND_* environment variables. It does not fail when AHASEND_API_KEY is unset — construction succeeds and every request fails instead, so check the variable yourself at startup. To override the key for one call, pass api.WithRequestAPIKey(key) as that method’s final request option. Keep context.Context for cancellation, deadlines, and request-scoped values rather than using it for optional parameters.
See API rate limits. Pacing is already on when you build the client, preset to the standard account limits: 100 requests per second with a 200 burst for sends and other general endpoints, 1 per second with no burst for statistics. These setters move a bucket to whatever limit your account actually has; they do not switch pacing on. That matters in both directions — an account provisioned above 100 sends per second stays capped at the default until you raise the bucket. The token buckets are local to one client instance, so they do not coordinate across application replicas and do not eliminate API 429 responses.

Errors

apiErr also carries Type, RequestID, Message, RetryAfter on 429 and idempotency conflicts, and the raw response body in Raw. Type is the field to branch and log on; the struct has a Code field, but the SDK never fills it. Do not rely on StatusCode alone. Failures raised before the request leaves the process — no API key configured, or a request body that fails client-side validation — are also *api.APIError, but they carry StatusCode 0 and only Type (api.ErrorTypeAuthentication, api.ErrorTypeValidation) identifies them. A switch on status code alone drops them silently, so branch on Type and keep a default arm. Treat Message, Raw, and err.Error() as sensitive: an API response can repeat addresses, content, headers, or other request data. Log only allowlisted fields such as status, error type, request ID, and aggregate counts. When retries are enabled, a surfaced 429 or server error has exhausted the configured retry attempts. Disabling retries or setting the retry count to zero makes the first response surface immediately.

Retries and Idempotency

Retries are on by default: the SDK retries network failures, 429, and 5xx responses with the configured backoff, and leaves every other 4xx alone unless you opt in with RetryConfig.RetryClientErrors. It attaches an idempotency key to POST operations and reuses that key across its own retry attempts.
Stored outcomes replay for 24 hours, covering 2xx and deterministic 4xx responses. Server errors are not stored, so a retry after a 5xx can still result in a second send. Pass your own key derived from a stable business identifier when a duplicate would be expensive. API key creation is the exception: because its response carries a one-time secret, that outcome replays for only 5 minutes, after which the same key creates a second credential.
Reuse a caller-provided key only for the same exact request payload, and never log it. If the original keyed request is still running, the API can return 409; the SDK surfaces this as api.ErrorTypeIdempotencyConflict with RetryAfter and does not retry it automatically. For email sends, reconcile the business operation or explicitly decide whether a possible duplicate is preferable to a possible missed send before retrying with the same key.

Services

Webhooks

ParseRequest verifies an *http.Request directly, which covers chi, Echo, Gin, and gorilla/mux since they all wrap net/http. It reads the body without imposing a size limit, so apply http.MaxBytesReader first. For fasthttp routers such as Fiber, enforce an equivalent limit, read the exact body bytes, and call verifier.Parse(body, headers) with an http.Header carrying webhook-id, webhook-timestamp, and webhook-signature.
ParseRequest consumes the request body, so verification fails if middleware has already read it without restoring the exact bytes. Reject an empty secret before constructing the verifier, and pass the value exactly as the dashboard shows it, aha-whsec- prefix included.
Signature timestamp validation is not replay protection: the same signed request can be delivered again inside the tolerance window. After verification, atomically commit the webhook-id and a durable work/outbox record, retaining IDs for at least the webhook delivery and retry horizon. A duplicate should receive 2xx without running the handler again; a storage failure should receive 5xx. Process the durable work idempotently outside the request. Apply concurrency and request-rate limits at the reverse proxy so concurrent maximum-size bodies cannot exhaust the application. Never log the raw body, signature, event object, subject, recipient, or whole verification error. Events implement webhooks.WebhookEvent with GetType() and GetTimestamp(). The SDK covers the message, suppression, domain, and route event types. Helpers such as webhooks.IsMessageEvent() and webhooks.GetMessageEventData() handle common message events without a full type switch.

Framework Guides

Gin

Echo

Fiber

chi

gorilla/mux

Cloud Run Functions

Azure Functions is in the sidebar too. Building in Node.js instead? See the Node.js SDK.

Source and Support

MIT licensed, developed at github.com/AhaSend/ahasend-go, with reference docs on pkg.go.dev. The AhaSend CLI is built on this SDK. Use Go over SMTP if your app already has an SMTP client.