Quickstart · Go
Send your first email from a Go service on net/http. Bird Go client for the send, crypto/hmac for the webhook verify.
Prerequisites
- Go 1.22 or later
- A Bird test API key from the dashboard —
bird_test_xxxxxxxx
Install the SDK
go mod init example.com/bird-quickstart
go get github.com/bird-io/bird-go
Set the env vars
export BIRD_API_KEY=bird_test_xxxxxxxx
export BIRD_WEBHOOK_SECRET=whsec_xxxxxxxx
Send + receive in one file
Create main.go:
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/bird-io/bird-go"
)
var client = bird.New(os.Getenv("BIRD_API_KEY"))
type sendRequest struct {
To string `json:"to"`
}
type webhookEvent struct {
Type string `json:"type"`
Data struct {
ID string `json:"id"`
} `json:"data"`
}
func sendWelcome(w http.ResponseWriter, r *http.Request) {
var body sendRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "invalid_request", http.StatusBadRequest)
return
}
resp, err := client.Emails.Send(r.Context(), &bird.EmailSendParams{
From: "Bird <onboarding@bird.dev>",
To: body.To,
Subject: "Welcome",
HTML: "<p>It works.</p>",
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("content-type", "application/json")
w.WriteHeader(http.StatusAccepted)
json.NewEncoder(w).Encode(map[string]string{"id": resp.ID})
}
func birdWebhook(w http.ResponseWriter, r *http.Request) {
raw, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "invalid_request", http.StatusBadRequest)
return
}
parts := map[string]string{}
for _, p := range strings.Split(r.Header.Get("Bird-Signature"), ",") {
if kv := strings.SplitN(p, "=", 2); len(kv) == 2 {
parts[kv[0]] = kv[1]
}
}
ts, err := strconv.ParseInt(parts["t"], 10, 64)
if err != nil || parts["v1"] == "" {
http.Error(w, "invalid_signature", http.StatusUnauthorized)
return
}
if age := time.Now().Unix() - ts; age > 300 || age < -300 {
http.Error(w, "invalid_signature", http.StatusUnauthorized)
return
}
mac := hmac.New(sha256.New, []byte(os.Getenv("BIRD_WEBHOOK_SECRET")))
mac.Write([]byte(parts["t"] + "."))
mac.Write(raw)
expected := mac.Sum(nil)
sigBytes, err := hex.DecodeString(parts["v1"])
if err != nil || !hmac.Equal(sigBytes, expected) {
http.Error(w, "invalid_signature", http.StatusUnauthorized)
return
}
var event webhookEvent
if err := json.Unmarshal(raw, &event); err != nil {
http.Error(w, "invalid_payload", http.StatusBadRequest)
return
}
// event.Type == "email.delivered" | "email.bounced" | ...
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]bool{"received": true})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("POST /api/send-welcome", sendWelcome)
mux.HandleFunc("POST /webhooks/bird", birdWebhook)
log.Println("listening on http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
Run it:
go run main.go
curl -X POST http://localhost:8080/api/send-welcome \
-H 'content-type: application/json' \
-d '{"to":"delivered@bird.dev"}'
What just happened
delivered@bird.dev is a sanctioned test recipient — it accepts the send, returns an email_* id, and emits the delivery event back to your webhook. The handler builds the same t.payload string the SDKs do, runs HMAC-SHA256 against the shared secret, and uses hmac.Equal for constant-time comparison.
Next steps
- Verify a sending domain — graduate from test keys to production sends.
- Send an SMS — same auth, same response envelope.
- Webhooks deep-dive — the full event catalog and retry schedule.
- Drop the MCP server in your IDE — let Claude and Cursor send through Bird.