# 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. For sending email with the SDK, start with the [Go email quickstart](/docs/get-started/quickstarts/go/email).

## Install

```bash
go get github.com/messagebird/bird-sdk-go
```

Requires Go 1.24+. Runnable per-method examples render under each symbol on [pkg.go.dev](https://pkg.go.dev/github.com/messagebird/bird-sdk-go).

## 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:

<!-- bird:snippet email.send -->

```go
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):

```go
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))
```

| Option                                                       | Scope                    | What it does                                                                                |
| ------------------------------------------------------------ | ------------------------ | ------------------------------------------------------------------------------------------- |
| `WithAPIKey`, `WithBaseURL`, `WithRegion`, `WithHTTPClient`  | Construction only        | Credentials, endpoint resolution, and the underlying `*http.Client`.                        |
| `WithTimeout`, `WithMaxRetries`                              | Construction or per call | Per-attempt timeout and the retry budget for transient failures.                            |
| `WithIdempotencyKey`                                         | Per call                 | Pin the idempotency key for one mutating call (one is generated for you otherwise).         |
| `WithHeader`                                                 | Construction or per call | Extra request headers. SDK-owned headers (`Authorization`, `Idempotency-Key`, …) win.       |
| `WithEmailDefaults`, `WithWebhookSecret`, `WithResponseInto` | Construction or per call | Channel-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](/docs/sdks/concepts).

## Errors

Every server failure is a `*bird.APIError` carrying `StatusCode`, `Type` (the coarse [error category](/docs/guides/errors)), `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`:

<!-- bird:snippet email.send.errors -->

```go
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:

<!-- bird:snippet email.list -->

```go
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](https://www.standardwebhooks.com/) 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:

<!-- bird:snippet webhook.unwrap -->

```go
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:

<!-- bird:snippet client.get -->

```go
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](/docs/api).

## Next steps

- [Go email quickstart](/docs/get-started/quickstarts/go/email) — first send in three steps: `Send`, `Get`, `List`, and every param.
- [SDK concepts](/docs/sdks/concepts) — the cross-SDK model: errors, idempotency, pagination, webhooks.
- [API reference](/docs/api) — the underlying HTTP contract.