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

Mulai dengan satu channel.
Tambahkan yang lain saat Anda siap.

API key uji coba langsung tersedia untuk Anda. Akses produksi terbuka setelah Anda menambahkan metode pembayaran dan memverifikasi pengirim.

Mulai sekarangBaca dokumentasiatau

Menggunakan Claude Code, Cursor, atau Codex? Arahkan ke MCP server kami — 141 tools, satu per API endpoint, dengan scoped agent keys.