Skip to main content
This guide shows how to use AhaSend to send transactional email from Echo in Go, keep credentials on the server, and test a sandbox send. Echo exposes the underlying request through c.Request(), which can be passed directly to verifier.ParseRequest. 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

  • 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 in From.Email, 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 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.

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
This is a server-to-server route. Never expose 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.
ParseRequest consumes the request body and must see its original bytes. Don’t call c.Bind first. Keep the route-specific BodyLimit: it wraps rather than pre-consumes the stream and prevents the Go SDK’s unbounded io.ReadAll from exhausting memory. Skip BodyDump because it captures the sensitive event body, even though Echo restores the stream afterward.
webhook.go
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). 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

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

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.
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.
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.
The From address must belong to a verified sending domain on your account. Check domain status in the dashboard.