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 the client itself — install, configuration, errors, retries, pagination, and webhooks. The per-method reference lives on the channel pages, starting with Email.

Install

Codevoorbeeld
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 region is inferred from the key's bk_{region}_… prefix, which selects the base URL:
Codevoorbeeld
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 and cancellation propagates verbatim (context.Canceled / context.DeadlineExceeded, never wrapped 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):
Codevoorbeeld
client, err := bird.NewClient(
  option.WithAPIKey(os.Getenv("BIRD_API_KEY")),
  option.WithTimeout(10*time.Second), // per-attempt; each retry gets a fresh budget
)

// Per-call override — disable retries for just this send.
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 a low-level client are generated from Bird's OpenAPI spec; package bird is a hand-written, idiomatic layer on top — a curated resource surface (client.Email, client.Webhooks), hand-written param structs with clean Go types ([]string, time.Time), and response types aliased to the generated models so they always match the wire. The request lifecycle — retries, timeouts, idempotency — lives in the core, not in any one resource, so every method behaves the same. The full picture of the API's shape is in SDK concepts.

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) — and both unwrap to *APIError, so a single errors.As(err, &apiErr) still catches everything the server returned. Branch with errors.As:
Codevoorbeeld
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 — timeouts, 429s, 5xx — retry automatically (default budget: 2 retries; tune with WithMaxRetries, zero disables). For mutating calls the SDK generates one idempotency key per logical call and reuses it across every retry attempt, so a retried send can never double-deliver. Pass option.WithIdempotencyKey to pin your own key — for example, to make your own application-level retries safe too.

Pagination

List methods return an iter.Seq2[*T, error] — a lazy, range-over-func iterator that fetches pages on demand as you consume it:
Codevoorbeeld
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), and hand Unwrap the exact bytes you received — parsing and re-serializing first breaks the signature:
Codevoorbeeld
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:
Codevoorbeeld
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