ahasend-go is the official Go SDK for AhaSend. Typed models for every endpoint, retries and idempotency keys handled for you, and a Standard Webhooks verifier that drops into any net/http router.
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.
Install
Send Your First Email
main.go
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
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.
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.
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.
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.

