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 dashboardbird_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

Begin met één kanaal.
Voeg de rest toe wanneer je er klaar voor bent.

Een test-API-key is direct beschikbaar. Productietoegang wordt ontgrendeld zodra je een betaalmethode toevoegt en een afzender verifieert.

Aan de slagLees de docsof

Gebruik je Claude Code, Cursor of Codex? Verwijs naar onze MCP-server — 141 tools, één per API-endpoint, met scoped agent keys.