c.Request(), which can be passed directly to verifier.ParseRequest.
Prerequisites
- Go 1.25 or newer, which Echo v5 requires
- An AhaSend account with a verified sending domain
- An API key scoped to
messages:send:{your-domain}, matching the domain inFrom.Email, and your account ID
Install the SDK
Configure Environment Variables
Create the Client
Create the client and verifier once at startup and share them across handlers. Fail startup if any required credential is missing:main.go
RequestLogger does not capture request bodies. Do not add BodyDump to the webhook route: it captures the complete signed event for its callback, which can expose recipient or message data in diagnostics.
Both routes carry a BodyLimit so no handler reads an unbounded body. On the send route the middleware order matters: authentication runs first, so unauthenticated traffic is rejected before it can consume the rate limiter’s budget and 429 legitimate callers.
Send an Email from an Echo Handler
Echo handlers take a*echo.Context and return an error. Bind the JSON body into a struct with c.Bind:
send.go
AHASEND_SEND_TOKEN to a browser; for browser-facing flows, use your application’s authentication and load the recipient from its trusted user record instead of accepting an arbitrary address.
The example defaults to sandbox mode, which validates without delivery. Change sandbox to false only when you intend to send real mail. The stable business-event key protects later calls for the same logical request, so keep an event_id bound to the same request data: AhaSend hashes the method, path, and body behind the key, so reusing one key for a changed payload returns 422 Unprocessable Entity rather than sending. The mode prefix keeps the sandbox and live attempts on separate keys, so flipping sandbox for the same event_id sends instead of hitting that 422.
The SDK also generates a key when none is supplied and reuses a request’s key across its automatic exponential-backoff retries. Stored outcomes replay for 24 hours, but a 5xx is not stored, so even a same-key retry after a server error can still send twice.
Handle Webhooks
Echo exposes the underlying*http.Request via c.Request(), so the SDK’s ParseRequest works directly: it verifies the HMAC signature and timestamp, then returns a typed event.
webhook.go
https://your-app.com/webhooks/ahasend, and copy its secret into AHASEND_WEBHOOK_SECRET exactly as shown (including the aha-whsec- prefix).
Timestamp tolerance is not replay deduplication: the same valid delivery can be replayed within the window. This example intentionally performs no business side effects. Before adding any, atomically commit the verified webhook-id together with durable queue/outbox work, acknowledge duplicates with 2xx, and process the durable work idempotently. Do not acknowledge and then start an in-process goroutine; a crash can lose the event permanently.
Going Further
- Templating: set
Substitutions: map[string]interface{}{"first_name": "Jane"}and use{{ first_name }}in the subject or body. - Batch sends:
Recipientsaccepts 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. - Rate limits: the client already throttles sends to AhaSend’s published limit of 100 requests per second with a 200-request burst, and the limit is per account rather than per key. Running several processes against one account? Use
ahasendClient.SetSendMessageRateLimit(requestsPerSecond, burst)to give each a share that adds up to no more than the account limit. - Other frameworks: the same SDK patterns work in Gin, Fiber, and chi.
- Attachments: add
AttachmentswithData,ContentType, andFileName. SetBase64: truefor binary files such as PDFs.
DomainsAPI, SuppressionsAPI, StatisticsAPI, and more).
Troubleshooting
401 from the API
401 from the API
The API key is missing, malformed, or revoked. Verify
AHASEND_API_KEY is set in the process environment and that the key exists in your dashboard.Webhook verification always fails
Webhook verification always fails
Ensure nothing called
c.Bind or otherwise consumed the body before ParseRequest. BodyLimit is compatible and should stay on the route; BodyDump restores the body but should be skipped because its callback receives the sensitive event payload. Also confirm the secret matches the dashboard exactly, including the aha-whsec- prefix.c.Bind rejects my request or leaves my struct empty
c.Bind rejects my request or leaves my struct empty
Echo binds by
Content-Type. With a body but no recognized Content-Type, c.Bind returns a 415 Unsupported Media Type error, which the handler above reports as a 400 — send Content-Type: application/json. A request with no body at all binds successfully and leaves every field zero-valued, which is why event_id and email are validated explicitly.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.
