Documentation
Sign inGet started

Go SDK

github.com/messagebird/bird-sdk-go (package bird) is the official Go SDK for the Bird API. This page covers installation, configuration, errors, retries, pagination, and webhooks. To send email with the SDK, start with the Go email quickstart.

Install

Exemplo de código
go get github.com/messagebird/bird-sdk-go
Requires Go 1.24+. Runnable per-method examples render under each symbol on pkg.go.dev.

Create a client

bird.NewClient takes functional options from the option package. Only the API key is required. The key's bk_{region}_… prefix selects the base URL:
Exemplo de código
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	bird "github.com/messagebird/bird-sdk-go"
	"github.com/messagebird/bird-sdk-go/option"
)

func main() {
	client, err := bird.NewClient(option.WithAPIKey(os.Getenv("BIRD_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}
	msg, err := client.Email.Send(context.Background(), bird.EmailSendParams{
		From:    "onboarding@messagebird.dev",
		To:      []string{"delivered@messagebird.dev"},
		Subject: "Hello from Bird",
		HTML:    "<p>My first Bird email.</p>",
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(msg.Id, *msg.Status)
}
Every API method is context-first and returns (T, error). Pass your request's context to propagate cancellation as context.Canceled or context.DeadlineExceeded without wrapping it in an SDK error.

Options

Options apply in order (a later option wins). Four are construction-only and return an error if passed to a single call: WithAPIKey, WithBaseURL, WithRegion, and WithHTTPClient. Everything else works both at construction (a client-wide default) and per call (an override for that one request):
Exemplo de código
client, err := bird.NewClient(
  option.WithAPIKey(os.Getenv("BIRD_API_KEY")),
  option.WithTimeout(10*time.Second), // per-attempt; each retry gets a fresh budget
)

// This send does not retry transient failures.
msg, err := client.Email.Send(ctx, params, option.WithMaxRetries(0))
OptionScopeWhat it does
WithAPIKey, WithBaseURL, WithRegion, WithHTTPClientConstruction onlyCredentials, endpoint resolution, and the underlying *http.Client.
WithTimeout, WithMaxRetriesConstruction or per callPer-attempt timeout and the retry budget for transient failures.
WithIdempotencyKeyPer callPin the idempotency key for one mutating call (one is generated for you otherwise).
WithHeaderConstruction or per callExtra request headers. SDK-owned headers (Authorization, Idempotency-Key, …) win.
WithEmailDefaults, WithWebhookSecret, WithResponseIntoConstruction or per callChannel-wide send defaults, the webhook signing secret, and raw transport-metadata capture.

How it's built

The wire types and low-level client are generated from Bird's OpenAPI specification. The hand-written bird package provides the curated resource surface (client.Email, client.Webhooks) and parameter structs with Go types such as []string and time.Time. Its response types alias the generated models to stay aligned with the wire format. The core manages retries, timeouts, and idempotency for every resource. See SDK concepts for the full model.

Errors

Every server failure is a *bird.APIError carrying StatusCode, Type (the coarse error category), Code (the stable E##### code), Message, and RequestID for support correlation. Two variants carry extra data: *bird.RateLimitError (a 429, with RetryAfter) and *bird.ValidationError (a 422, with per-field Details). Both unwrap to *APIError, so a single errors.As(err, &apiErr) catches every server response. Branch with errors.As:
Exemplo de código
package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"os"

	bird "github.com/messagebird/bird-sdk-go"
	"github.com/messagebird/bird-sdk-go/option"
)

func main() {
	client, err := bird.NewClient(option.WithAPIKey(os.Getenv("BIRD_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}
	_, err = client.Email.Send(context.Background(), bird.EmailSendParams{
		From: "onboarding@messagebird.dev", To: []string{"delivered@messagebird.dev"}, Subject: "Hello from Bird", HTML: "<p>My first Bird email.</p>",
	})
	if err != nil {
		var rle *bird.RateLimitError
		var ve *bird.ValidationError
		var ae *bird.APIError
		switch {
		case errors.As(err, &rle):
			fmt.Println("rate limited; retry after", rle.RetryAfter)
		case errors.As(err, &ve):
			for _, d := range ve.Details {
				fmt.Printf("%s: %s\n", d.Param, d.Message)
			}
		case errors.As(err, &ae):
			fmt.Printf("API error %s (status %d, request %s)\n", ae.Code, ae.StatusCode, ae.RequestID)
		default:
			log.Print(err) // transport: *bird.ConnectionError or *bird.TimeoutError
		}
	}
}
Failures with no HTTP response are separate types: *bird.ConnectionError (DNS, connection refused) and *bird.TimeoutError (a single attempt exceeded its timeout). A bad webhook signature is *bird.WebhookVerificationError.

Safe retries

Transient failures, including timeouts, 429 responses, and 5xx responses, retry automatically. The default budget is two retries; tune it with WithMaxRetries, or use zero to disable retries. For mutating calls, the SDK generates one idempotency key per logical call and reuses it across every attempt. Pass option.WithIdempotencyKey to set your own key and make application-level retries safe.

Pagination

List methods return an iter.Seq2[*T, error], a lazy range-over-func iterator that fetches pages as you consume it:
Exemplo de código
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	bird "github.com/messagebird/bird-sdk-go"
	"github.com/messagebird/bird-sdk-go/option"
)

func main() {
	client, err := bird.NewClient(option.WithAPIKey(os.Getenv("BIRD_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}
	for msg, err := range client.Email.List(context.Background(), bird.EmailListParams{Status: bird.EmailStatusBounced}) {
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(msg.Id)
	}
	page, err := client.Email.ListPage(context.Background(), bird.EmailListParams{}, "")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(page.Data)) // page.NextCursor carries the next starting_after
}
Breaking out of the loop stops fetching; a fetch error is yielded once and ends the sequence. For manual cursor control, ListPage returns one page plus the next cursor.

Webhooks

client.Webhooks.Unwrap verifies a Standard Webhooks signature over the raw request body and returns a typed event. Configure the signing secret with option.WithWebhookSecret on the client or per call. Pass Unwrap the exact bytes you received because parsing and re-serializing them breaks the signature:
Exemplo de código
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"

	bird "github.com/messagebird/bird-sdk-go"
	"github.com/messagebird/bird-sdk-go/option"
)

func main() {
	client, err := bird.NewClient(
		option.WithAPIKey(os.Getenv("BIRD_API_KEY")),
		option.WithWebhookSecret(os.Getenv("BIRD_WEBHOOK_SECRET")),
	)
	if err != nil {
		log.Fatal(err)
	}
	http.HandleFunc("/webhooks/bird", func(w http.ResponseWriter, r *http.Request) {
		body, _ := io.ReadAll(r.Body)
		event, err := client.Webhooks.Unwrap(body, r.Header)
		if err != nil {
			http.Error(w, "invalid signature", http.StatusBadRequest)
			return
		}
		w.WriteHeader(http.StatusNoContent) // ack fast, then process

		payload, _ := event.AsAny()
		switch p := payload.(type) {
		case bird.EmailDeliveredEvent:
			fmt.Println("delivered:", p.Data.EmailId, p.Data.Recipient)
		case bird.EmailBouncedEvent:
			fmt.Println("bounced:", p.Type)
		}
	})
}
Switch on event.Type() for the discriminant, or AsAny() for the concrete payload. An unknown future event type returns an error rather than panicking, so an older SDK keeps working against a newer server.

Escape hatch

Endpoints not yet on the typed surface are reachable through client.Get / Post / Put / Patch / Delete, with the same auth, retries, and idempotency handling:
Exemplo de código
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	bird "github.com/messagebird/bird-sdk-go"
	"github.com/messagebird/bird-sdk-go/option"
)

func main() {
	client, err := bird.NewClient(option.WithAPIKey(os.Getenv("BIRD_API_KEY")))
	if err != nil {
		log.Fatal(err)
	}
	var out struct {
		Data []struct {
			Recipient string `json:"recipient"`
		} `json:"data"`
	}
	if err := client.Get(context.Background(), "/v1/email/suppressions", &out); err != nil {
		log.Fatal(err)
	}
	fmt.Println(len(out.Data))
}
Find the paths in the API reference.

Next steps