Skip to content
Docs

Tutorial

Send email from Go with the emailit-go SDK or net/smtp, handle API errors, and verify Emailit webhook signatures in a net/http handler.

Updated Oct 1, 2026

This guide shows how to send email from a Go service with the official emailit-go SDK, how to use net/smtp instead, and how to verify webhooks in a net/http handler.

Prerequisites

  • Go 1.21 or later.
  • A verified sending domain, for example acme.com.
  • An API key. A Sending Only key restricted to your domain is enough.
  • Until your workspace has production access, you can only send to the account emails of workspace members.

Install the SDK

Terminal
go get github.com/emailit/emailit-go/v2

The SDK only depends on the Go standard library.

Configure your API key

Pass the key to your process as an environment variable:

Terminal
export EMAILIT_API_KEY=secret_••••••••••••••••••••••••••••••••

Send an email

main.go
package main

import (
	"log"
	"os"

	"github.com/emailit/emailit-go/v2"
)

func main() {
	client := emailit.NewClient(os.Getenv("EMAILIT_API_KEY"))

	email, err := client.Emails.Send(&emailit.SendEmailRequest{
		From:    "Acme <hello@acme.com>",
		To:      []string{"ada@example.com"},
		Subject: "Your login code",
		Text:    "Your code is 482913. It expires in 10 minutes.",
		Html:    "<p>Your code is <strong>482913</strong>. It expires in 10 minutes.</p>",
	})
	if err != nil {
		log.Fatal(err)
	}

	log.Println("sent", email.Id) // em_…
}

Create the client once and share it; it’s safe to reuse across requests. To send a stored template, set Template to its alias or tem_ ID and pass Variables. To change timeouts, pass your own client: emailit.NewClient(key, emailit.WithHTTPClient(&http.Client{Timeout: 10 * time.Second})).

Handle errors

The SDK returns ordinary Go errors with helpers to classify them:

Go
email, err := client.Emails.Send(req)
if err != nil {
	switch {
	case emailit.IsRateLimitError(err):
		// 429: back off and retry
	case emailit.IsAuthenticationError(err):
		// 401: the API key is missing or invalid
	case emailit.IsUnprocessableEntityError(err):
		// 422: for example, the from domain isn't verified
	}

	var apiErr *emailit.APIError
	if errors.As(err, &apiErr) {
		log.Printf("emailit: %d %s", apiErr.StatusCode, apiErr.Message)
	}
	return err
}

Send with SMTP instead

The standard library’s net/smtp can send through the Emailit relay. smtp.SendMail upgrades the connection with STARTTLS before it authenticates:

smtp.go
package main

import (
	"log"
	"net/smtp"
	"os"
	"strings"
)

func main() {
	auth := smtp.PlainAuth("", "emailit", os.Getenv("EMAILIT_API_KEY"), "smtp.emailit.com")

	msg := strings.Join([]string{
		"From: Acme <hello@acme.com>",
		"To: ada@example.com",
		"Subject: Your login code",
		"MIME-Version: 1.0",
		"Content-Type: text/plain; charset=UTF-8",
		"",
		"Your code is 482913. It expires in 10 minutes.",
	}, "\r\n")

	err := smtp.SendMail("smtp.emailit.com:587", auth, "hello@acme.com", []string{"ada@example.com"}, []byte(msg))
	if err != nil {
		log.Fatal(err)
	}
}

net/smtp is minimal: you build the message yourself, and it has no implicit TLS on port 465. For HTML, attachments or connection reuse, use a maintained mail library or the API. If port 587 is blocked, use smtp.emailit.com:2525. See SMTP settings.

Receive webhooks

Create a webhook and store its signing secret in EMAILIT_WEBHOOK_SECRET. The handler reads the raw body, checks the signature, then decodes the array of events:

webhooks.go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"io"
	"net/http"
	"os"
	"strconv"
	"time"
)

type emailitEvent struct {
	EventID string `json:"event_id"`
	Type    string `json:"type"`
	Data    struct {
		Object map[string]any `json:"object"`
	} `json:"data"`
}

func validSignature(body []byte, signature, timestamp, secret string) bool {
	ts, err := strconv.ParseInt(timestamp, 10, 64)
	if err != nil || signature == "" {
		return false
	}
	if age := time.Now().Unix() - ts; age > 300 || age < -300 {
		return false
	}
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(timestamp + "."))
	mac.Write(body)
	expected := hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(expected), []byte(signature))
}

func emailitWebhook(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 10<<20))
	if err != nil {
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}

	if !validSignature(body, r.Header.Get("X-Emailit-Signature"), r.Header.Get("X-Emailit-Timestamp"), os.Getenv("EMAILIT_WEBHOOK_SECRET")) {
		http.Error(w, "invalid signature", http.StatusUnauthorized)
		return
	}

	var events []emailitEvent // up to 100 events per request
	if err := json.Unmarshal(body, &events); err != nil {
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}

	for _, event := range events {
		if event.Type == "email.bounced" {
			// Stop emailing event.Data.Object["to"].
		}
	}

	w.WriteHeader(http.StatusOK)
}

func main() {
	http.HandleFunc("POST /webhooks/emailit", emailitWebhook)
	http.ListenAndServe(":8080", nil)
}

The POST /path pattern needs Go 1.22 or later; on Go 1.21, register /webhooks/emailit and check r.Method yourself. Return a 2xx within 30 seconds; other responses are retried. See Request signature.

Production tips

  • Stay under your rate limit. New workspaces can send 2 emails per second and 5,000 per day by default. Send bulk mail from a worker with a rate limiter such as golang.org/x/time/rate. See Limits.
  • Make retries safe. If you retry after a timeout, send an Idempotency-Key header with net/http so the email isn’t sent twice. See Idempotency.
  • Deduplicate webhooks by storing each EventID you process.
  • Set timeouts. Use a context or an HTTP client timeout so a slow network doesn’t hold request goroutines.

Next steps

Attachments, scheduling and tracking.
Headers and how to back off.
Every event and its payload.
All official libraries.

Was this page helpful?

Thanks for the feedback.

Thanks, we read every message.