AhaSend
Back to Blog

The Complete Guide to sending Transactional Emails in Go with AhaSend

Mark Kraakman
Mark Kraakman
Guides

Go developers have two solid options for sending transactional email with AhaSend: the HTTP API via the official Go SDK (recommended for most applications) and SMTP relay (useful when you're working with existing Go email libraries). This guide covers both, from installation to a production-ready example with error handling and webhook processing.

Prerequisites

  • Go 1.18 or later
  • An AhaSend account (free tier available, no credit card required)
  • A verified sending domain

If you haven't set up your domain yet, follow the domain configuration guide first. You cannot send from an unverified domain.

Option 1: HTTP API with the AhaSend Go SDK

The Go SDK is the recommended approach. It gives you full API coverage, built-in rate limiting, automatic retries with exponential backoff, idempotency protection, and webhook verification out of the box.

Install the SDK

go get github.com/AhaSend/ahasend-go

The SDK has minimal dependencies - only github.com/google/uuid and github.com/stretchr/testify (for tests).

Set your credentials

Store credentials as environment variables, never in source code:

export AHASEND_API_KEY="aha-sk-your-64-character-key"
export AHASEND_ACCOUNT_ID="your-account-id-here"

You'll find both values in your AhaSend dashboard.

Send your first email

package main

import (
    "context"
    "log"
    "os"

    "github.com/AhaSend/ahasend-go"
    "github.com/google/uuid"
)

func main() {
    apiKey := os.Getenv("AHASEND_API_KEY")
    accountID := uuid.MustParse(os.Getenv("AHASEND_ACCOUNT_ID"))

    client := ahasend.NewAPIClient(ahasend.NewConfiguration())
    ctx := context.WithValue(context.Background(),
        ahasend.ContextAccessToken, apiKey)

    message := ahasend.CreateMessageRequest{
        From: ahasend.SenderAddress{Email: "[email protected]"},
        Recipients: []ahasend.Recipient{
            {Email: "[email protected]"},
        },
        Subject:     "Welcome to Your App",
        HtmlContent: ahasend.PtrString("<h1>Welcome!</h1><p>Thanks for signing up.</p>"),
        TextContent: ahasend.PtrString("Welcome! Thanks for signing up."),
    }

    response, _, err := client.MessagesAPI.CreateMessage(ctx, accountID, message)
    if err != nil {
        log.Fatalf("Failed to send email: %v", err)
    }

    log.Printf("Email sent. Message ID: %s", *response.Data[0].Id)
}

Set the API key at client level

For applications sending many emails, set the API key once on the client rather than passing it through context on every request:

client := ahasend.NewAPIClient(
    ahasend.WithAPIKey(os.Getenv("AHASEND_API_KEY")),
)

// All subsequent calls use this key automatically - no ctx override needed
response, _, err := client.MessagesAPI.CreateMessage(ctx, accountID, message)

A real-world example: user signup confirmation

In practice you'll be sending email as part of an application flow. Here's a signup confirmation inside a Go HTTP handler, with proper error handling:

package handlers

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"

    "github.com/AhaSend/ahasend-go"
    "github.com/google/uuid"
)

var (
    emailClient *ahasend.APIClient
    accountID   uuid.UUID
)

func init() {
    emailClient = ahasend.NewAPIClient(
        ahasend.WithAPIKey(os.Getenv("AHASEND_API_KEY")),
    )
    accountID = uuid.MustParse(os.Getenv("AHASEND_ACCOUNT_ID"))
}

func sendWelcomeEmail(ctx context.Context, userEmail, userName string) error {
    htmlBody := fmt.Sprintf(`
        <h1>Welcome, %s!</h1>
        <p>Your account is ready. Log in at any time.</p>
        <p>Questions? Just reply to this email.</p>
    `, userName)

    textBody := fmt.Sprintf(
        "Welcome, %s! Your account is ready. Questions? Just reply to this email.",
        userName,
    )

    message := ahasend.CreateMessageRequest{
        From: ahasend.SenderAddress{
            Email: "[email protected]",
            Name:  ahasend.PtrString("Your App"),
        },
        Recipients: []ahasend.Recipient{
            {
                Email: userEmail,
                Name:  ahasend.PtrString(userName),
            },
        },
        Subject:     "Welcome to Your App",
        HtmlContent: ahasend.PtrString(htmlBody),
        TextContent: ahasend.PtrString(textBody),
    }

    _, _, err := emailClient.MessagesAPI.CreateMessage(ctx, accountID, message)
    return err
}

func RegisterHandler(w http.ResponseWriter, r *http.Request) {
    // ... your registration logic ...

    if err := sendWelcomeEmail(r.Context(), newUser.Email, newUser.Name); err != nil {
        // Log but don't fail the registration. A transient email issue
        // shouldn't block account creation. Track delivery via webhooks.
        log.Printf("Warning: failed to send welcome email to %s: %v", newUser.Email, err)
    }

    w.WriteHeader(http.StatusCreated)
}

Handling delivery events with webhooks

The SDK includes Standard Webhooks compliant processing with HMAC-SHA256 signature verification. Configure a webhook endpoint in your AhaSend dashboard, set the secret as an environment variable, then handle events:

package main

import (
    "log"
    "net/http"
    "os"

    "github.com/AhaSend/ahasend-go/webhooks"
)

func main() {
    verifier, err := webhooks.NewWebhookVerifier(os.Getenv("AHASEND_WEBHOOK_SECRET"))
    if err != nil {
        log.Fatalf("Failed to create verifier: %v", err)
    }

    http.HandleFunc("/webhooks/ahasend", func(w http.ResponseWriter, r *http.Request) {
        event, err := verifier.ParseRequest(r)
        if err != nil {
            http.Error(w, "Invalid webhook", http.StatusBadRequest)
            return
        }

        switch e := event.(type) {
        case *webhooks.MessageDeliveredEvent:
            log.Printf("Delivered to %s", e.Data.Recipient)
        case *webhooks.MessageBouncedEvent:
            // Mark address as undeliverable in your database
            log.Printf("Bounced: %s - reason: %s", e.Data.Recipient, e.Data.Reason)
        case *webhooks.MessageOpenedEvent:
            log.Printf("Opened by %s", e.Data.Recipient)
        case *webhooks.MessageClickedEvent:
            log.Printf("Link clicked by %s", e.Data.Recipient)
        }

        w.WriteHeader(http.StatusOK)
    })

    log.Println("Webhook server listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Supported event types: message.delivered, message.bounced, message.opened, message.clicked, plus suppression.*, domain.*, and route.* events.

For the complete webhook server example, see webhook_processing.go in the SDK repository.

Rate limiting and retry configuration

The SDK manages rate limiting automatically. For high-volume workloads you can tune it:

// Configure for high-volume sending
client.SetSendMessageRateLimit(500, 1000) // 500 req/s, burst of 1000

// Configure retry behaviour
config := &ahasend.RetryConfig{
    Enabled:         true,
    MaxRetries:      3,
    BackoffStrategy: ahasend.ExponentialBackoff,
    BaseDelay:       time.Second,
    MaxDelay:        30 * time.Second,
}
client.SetRetryConfig(config)

Option 2: SMTP with gomail

If you're migrating from another provider and want to change as little code as possible, or your framework already has SMTP support built in, use gomail with AhaSend's SMTP relay.

Install gomail

go get gopkg.in/gomail.v2

SMTP settings

Host:     send.ahasend.com
Ports:    25, 587, 2525 (all support STARTTLS)
Auth:     Required

SMTP credentials are separate from your API key. Create them in your dashboard under Credentials.

Send via SMTP

package main

import (
    "fmt"
    "log"
    "os"

    gomail "gopkg.in/gomail.v2"
)

func main() {
    m := gomail.NewMessage()
    m.SetAddressHeader("From", "[email protected]", "Your App")
    m.SetAddressHeader("To", "[email protected]", "Recipient Name")
    m.SetHeader("Subject", "Hello from AhaSend")
    m.SetBody("text/plain", "This email was sent via AhaSend's SMTP relay.")
    m.AddAlternative("text/html", "<p>This email was sent via AhaSend's SMTP relay.</p>")

    d := gomail.NewDialer(
        "send.ahasend.com",
        587,
        os.Getenv("AHASEND_SMTP_USER"),
        os.Getenv("AHASEND_SMTP_PASSWORD"),
    )

    if err := d.DialAndSend(m); err != nil {
        log.Fatalf("Failed to send: %v", err)
    }

    fmt.Println("Email sent.")
}

 

Which method should you use?

Use the Go SDK if you're building a new application or want full visibility into delivery status, bounce handling, webhook events, and delivery statistics. The SDK handles retries, rate limiting, and idempotency so you don't have to.

Use SMTP if you're migrating from another provider and want to swap out credentials with minimal code changes.

Production-ready examples

The SDK repository includes 11 complete, runnable examples:

Run any example:

export AHASEND_API_KEY="your-api-key"
export AHASEND_ACCOUNT_ID="your-account-id"

go run examples/send_email.go

Note: all example files use //go:build ignore so they can be run individually without conflicting with go build ./....

Next steps

Questions? Reach us at [email protected] or join the Discord.