SDK concepts
The TypeScript, Go, and Python SDKs are built to one design. Each is two layers: a generated base (types and a low-level client produced from Bird's OpenAPI spec, never hand-edited) and a hand-written ergonomic layer on top that owns the request lifecycle and the curated surface. The generated layer guarantees contract accuracy; the hand-written layer is where everything on this page lives. This page explains the shared behavior once — the per-language pages cover only what is idiomatic to each.
Auto-idempotency
Every mutation (POST, PATCH, DELETE) gets an auto-generated Idempotency-Key header. The key is generated once per logical call and reused across every retry attempt — a hard invariant in all three SDKs — so a retried write never double-applies. If a send times out and the retry lands on a request the server already processed, the server replays the stored response instead of sending again. Pass your own key (per-call idempotencyKey / option.WithIdempotencyKey / idempotency_key) when the logical operation spans more than one SDK call, for example your own retry loop around the SDK. The server-side protocol is described in Idempotency.
Safe retries
Retries are on by default (maxRetries: 2 in every SDK). The client retries transient failures only — network errors, per-attempt timeouts, 429, and retryable 5xx — with jittered exponential backoff, and it honors the server's Retry-After header when one is present. Deterministic failures (401, 404, 422, and other 4xx) are never retried. Because the idempotency key is reused across attempts, retrying mutations is as safe as retrying reads. The timeout is per attempt (default 60 seconds), not per logical call: a call with retries enabled can take longer end-to-end than one attempt's timeout.
Pagination
List endpoints are cursor-paginated. Every SDK exposes the same two modes: native iteration that transparently fetches page after page, and a single-page accessor for manual cursor control (the page carries data and next_cursor; pass the cursor back as starting_after to advance).
Codebeispiel
for await (const message of bird.email.list({ status: "bounced" })) {
console.log(message.id);
}
const page = await bird.email.list({ limit: 50 }); // page.data, page.next_cursorCodebeispiel
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_afterCodebeispiel
for message in client.email.list(status="delivered"):
print(message.id)
page = client.email.list(status="delivered") # page.data, page.next_cursor
print(len(page.data), page.next_cursor)Region inference
Bird API keys encode their region: bk_{region}_{token}. The SDK reads the prefix and routes to https://{region}.platform.bird.com automatically, so a client constructed with only an API key already talks to the right region. Two overrides exist: a region option replaces the inferred region, and an explicit baseUrl (option.WithBaseURL / base_url) wins over both — that is the path for local development and self-hosted deployments. A key that doesn't match the bk_{region}_ format with no override is a construction error, not a fallback to a default region.
Per-call options vs construction-only config
Configuration splits into two tiers. Identity and transport — the API key, base URL/region, and the HTTP client or fetch implementation — are construction-only: they define which client you have and cannot change per call. Lifecycle knobs — timeout, maxRetries, the idempotency key, and extra headers — are set at construction as defaults and overridable on any individual call (a trailing options object in TypeScript and Python, option.With… variadic options in Go). SDK-owned headers (Authorization, User-Agent, Idempotency-Key) always win over caller-supplied ones. Channel defaults (for example a default email from) follow the same pattern: set once at construction, and any per-call value wins.
Webhook verification
Each SDK ships one verification entry point — webhooks.unwrap(rawBody, headers) — and it is the only crypto in the SDK. It implements Standard Webhooks: HMAC-SHA256 over the raw payload with your endpoint's signing secret, accepting only v1-tagged signature entries, rejecting timestamps outside a 5-minute tolerance window (replay protection), and comparing signatures in constant time (timing-attack protection). Always pass the raw request body bytes exactly as received — parsing and re-serializing the JSON changes the bytes and breaks the signature.
On success, unwrap returns a typed event discriminated on type (email.delivered, email.bounced, …). Unknown event types verify and decode rather than fail, so a newer server event never breaks an older SDK — handle them in your default branch. Verification failure is a distinct error (BirdWebhookVerificationError / *WebhookVerificationError / WebhookVerificationError); respond 400 and move on. Endpoint setup and the event catalog live in Webhooks.
User-Agent
Every official SDK identifies itself on each request: bird-sdk-js/{version}, bird-sdk-go/{version}, and bird-sdk-python/{version} ({runtime}/{runtime-version}). Bird never validates or requires the header — it exists so support can tell which SDK and version produced a request from logs alone. Calling the API directly with curl or bare fetch() without a User-Agent works fine. The header is SDK-owned and cannot be overridden through the extra-headers options.