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

Empieza con un canal.
Añade los demás cuando estés listo.

Una clave API de prueba es tuya de inmediato. El acceso a producción se desbloquea cuando añades un método de pago y verificas un remitente.

ComenzarLeer documentacióno

¿Usas Claude Code, Cursor o Codex? Apúntalos a nuestro servidor MCP — 141 herramientas, una por endpoint de API, con claves de agente con permisos limitados.