# Bird Documentation > The complete Bird documentation corpus, concatenated as Markdown for machine consumption. See https://bird.com/llms.txt for the indexed version with one-line summaries. This file contains 137 pages. Each page below opens with a frontmatter block (title, description, source URL) that delimits it from the previous page. --- title: "Get started with Bird" description: "Email, SMS, voice, and WhatsApp on one platform. Pick a use-case quickstart and go from zero to your first send." source: https://bird.com/en-us/docs --- Introduction # Get started with Bird Email, SMS, voice, and WhatsApp on one platform — per-channel endpoints over shared infrastructure. Create a key, send your first message, then wire up the rest at your own pace. Quickstart Get an API key View API Reference ## Choose a path Pick a use-case quickstart to go from zero to your first send. Every quickstart is one hand-held happy path — the canonical home that channel and product pages link to. ## Explore the docs Beyond the quickstarts, the documentation is organized into four areas. The **Guides** explain how Bird works and go channel-deep; the **Knowledge base** is the operational, how-do-I side; the **SDKs & CLI** are the official ways to call Bird from your own code; and the **API reference** documents every endpoint. ## The Email guide Email is the deepest section today. Start at the [Email overview](/docs/guides/email/overview) for the conceptual map, or jump straight to a topic: - **Sending** — [Sending email](/docs/guides/email/sending-email), [Bulk sending](/docs/guides/email/sending-bulk), [Categories](/docs/guides/email/categories), [Rate limits](/docs/guides/email/rate-limits) - **Domains & authentication** — [Sending domains](/docs/guides/email/sending-domains), [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc), [BIMI](/docs/guides/email/bimi) - **Reputation** — [Dedicated IPs & pools](/docs/guides/email/dedicated-ips-and-pools), [IP warmup](/docs/guides/email/ip-warmup), [Suppressions](/docs/guides/email/suppressions), [Deliverability](/docs/guides/email/deliverability) - **Visibility & testing** — [Tracking & metrics](/docs/guides/email/tracking-and-metrics), [Events](/docs/guides/email/events), [Mail sandbox](/docs/guides/email/testing-sandbox) - **Migrating** — from [SendGrid](/docs/guides/email/migrate/sendgrid), [Mailgun](/docs/guides/email/migrate/mailgun), [Amazon SES](/docs/guides/email/migrate/ses), or [Resend](/docs/guides/email/migrate/resend) ## The SMS guide SMS is in early access. Start at the [SMS overview](/docs/guides/sms/overview) for the map, or jump to [Sending SMS](/docs/guides/sms/sending-sms), the [SMS log](/docs/guides/sms/sms-log), [Tracking & metrics](/docs/guides/sms/tracking-and-metrics), or [Events](/docs/guides/sms/events). ## Next steps After your first send, wire up [Webhooks & events](/docs/guides/webhooks) to receive delivery events. --- title: "Agent skills" description: "Prebuilt skill files that teach a coding agent the bird CLI workflows: send and inspect email, manage sending domains, and manage webhook endpoints." source: https://bird.com/en-us/docs/ai/agent-skills --- # Agent skills Bird publishes **agent skills**: packaged procedure files a coding agent loads that teach it the [`bird` CLI](/docs/ai/cli-for-agents) workflows. Instead of the agent rediscovering flags and failure modes from `--help` output, a skill hands it the operation's happy path, the state checks to run first, and the traps that waste loop iterations — so the agent gets to a correct command on the first try, not the fifth. They ship as the **`bird-ai` marketplace plugin**, which installs the skills into Claude Code, Cursor, Codex, GitHub Copilot, and Factory Droid from one source — each reads the plugin's skills natively. On Claude Code, installing the plugin also wires up the [hosted MCP server](/docs/ai/mcp-server) (see [Skills, the plugin, and MCP](#skills-the-plugin-and-mcp) below). Each skill encodes **one operation per task**. The agent picks the one that matches the request; there is no ordering among them beyond the shared authentication prerequisite below. ## Install the plugin The marketplace lives at `messagebird/bird-ai`. On Claude Code, Copilot, and Droid — which read the Claude-format plugin manifest directly: ```text /plugin marketplace add messagebird/bird-ai /plugin install bird ``` On Cursor, add the marketplace and install the `bird` plugin from **Settings → Plugins** (or `/add-plugin bird`). On Codex, run `codex plugin marketplace add messagebird/bird-ai`, then `/plugins` inside `codex` and install the **bird** plugin. The skills install natively in every case. ## The operations - **Send and inspect email** — send a message with `bird email send`, then answer "did it bounce?" with `bird email get ` or `bird email list --status bounced`. The skill encodes the critical distinction: a send returns `202` with `status: accepted`, which means Bird took the message — not that it landed. Delivery is asynchronous, so the skill teaches the agent to read the message back for the real outcome instead of declaring victory at `accepted`. - **Manage sending domains, and find a verified `from`** — a message's `from` address must be on a **verified** sending domain, so before any send the skill has the agent find a usable sender (`bird email domains list`, filtering for a verified sending capability) or run the setup loop: `bird email domains create` → add the returned DNS records → `bird email domains verify` → repeat until `verified`. It also encodes the trap that DNS propagation is asynchronous — verifying immediately after creating almost always still reads `pending`. - **Manage outbound webhook endpoints** — register, list, inspect, test, and delete the endpoints Bird delivers events to. The skill encodes the create-time-only signing secret (capture it from the create response; it is never returned again), and that `bird webhooks test` makes a real delivery to the live URL. ## The shared prerequisite: authenticate first Every operation hits the **live Bird API**, so every skill starts the same way: confirm credentials with `bird auth status` before doing anything else. The check is idempotent — a no-op when the CLI already reports `valid: true` — so it is safe (and recommended) to run first every time. Without it, a missing login fails identically to a real API error, and the agent burns iterations debugging the wrong problem. ```bash bird auth status --format json # gate on "valid": true, then run the operation ``` If credentials are missing, the skill routes the agent through `bird auth login` (browser-based, with a device-code flow for headless hosts) and back to the task — it never stalls at "please authenticate". ## Failures surface the same way everywhere Because every operation is a thin wrapper over the live API, failures come back through the CLI's uniform contract rather than per-skill error handling: - **JSON by default** — successes print structured JSON to stdout, errors go to stderr, so the agent's loop can parse outcomes without scraping prose. - **Semantic exit codes** — `2` invalid usage or input, `3` not found, `4` auth or permission denied, `1` anything else. The agent branches on the category without parsing the message: exit `4` means re-run the auth step; exit `3` means the resource ID is wrong, and retrying won't help. This is the same contract the CLI presents to humans and scripts — the skills add no layer on top, they just teach the agent to use it. The full contract, including output formats and configuration, is on the [CLI for agents](/docs/ai/cli-for-agents) page. ## Composing skills into an agent loop Because each skill is one self-checking operation with a machine-readable outcome, they compose into a loop without glue code. A realistic task — "send the launch email and confirm it delivered" — decomposes as: 1. **Authenticate** — `bird auth status`; log in only if needed. 2. **Find a sender** — domains skill: pick a `from` on a verified domain (exit `0` + a verified domain in the JSON, or fall into the create-and-verify loop). 3. **Send** — email skill: `bird email send …`; success is `202` with an `em_…` ID and `status: accepted`. 4. **Confirm the outcome** — email skill again: `bird email get ` until the counts show `delivered` (or `bounced`, in which case the agent reports the failure instead of guessing). Each step's "done when" condition is checkable from the previous step's JSON output, which is what makes the loop reliable: the agent never has to infer state from prose. ## Skills, the plugin, and MCP Skills are one of three ways to point an agent at Bird, and they layer rather than compete: - The [`bird` CLI](/docs/ai/cli-for-agents) is the execution surface — skills assume a shell-capable agent that can run it. - The [MCP server](/docs/ai/mcp-server) is the alternative for agents that call tools instead of running commands; the operations are equivalent, the transport differs. - [AI onboarding](/docs/get-started/ai-onboarding) is the hand-held setup path that gets either one connected in minutes. Whether the plugin install also configures the MCP server depends on the client. Claude Code lets a plugin declare a remote MCP server, so installing `bird-ai` there connects you to `https://mcp.platform.bird.com` automatically. The other clients support remote MCP but don't let a plugin pre-declare it, so on Cursor, Codex, Copilot, and Droid the plugin ships the skills and you add the MCP server once by hand — the one-line config is on the [MCP server](/docs/ai/mcp-server) page. | Client | Skills via plugin | MCP server bundled | | -------------- | ----------------- | ---------------------------------------------------- | | Claude Code | Yes | Yes — connects to `mcp.platform.bird.com` on install | | Cursor | Yes | Manual — add the remote server once | | Codex | Yes | Manual — add the remote server once | | GitHub Copilot | Yes | Manual — add the remote server once | | Factory Droid | Yes | Manual — add the remote server once | ## Next steps - [Set up your coding agent](/docs/ai/set-up-your-agent) — the one-prompt setup that installs the plugin for you. - [MCP server](/docs/ai/mcp-server) — the tool surface the plugin bundles, and how to add it manually. - [CLI for agents](/docs/ai/cli-for-agents) — the command surface the skills teach, for shell-capable agents. - [AI onboarding](/docs/get-started/ai-onboarding) — the hand-held, end-to-end setup with the docs corpus wired in. --- title: "AI builder guides" description: "Patterns for building agents and apps on Bird email: the agent loop, async send outcomes, sandbox testing, error recovery, and safe retries." source: https://bird.com/en-us/docs/ai/ai-builder-guides --- # AI builder guides Bird's API surface is agent-shaped — one operation per tool, JSON in and out, machine-checkable outcomes — but a reliable agent still needs the right patterns around it. The five below cover the failure modes that actually break agent integrations: treating acceptance as delivery, retrying blind, and parsing prose instead of structure. Each pattern works the same whether your agent drives the [MCP server](/docs/ai/mcp-server) or the [`bird` CLI](/docs/ai/cli-for-agents). ## Pattern 1: Loop one operation at a time Bird's tools are deliberately granular — send a message, get a message, list domains, create a webhook endpoint — and every one returns structured JSON whose fields the next step can check. Build the loop so each step's exit condition is read from the previous step's output, never inferred: ```text loop: result = run_tool(next_operation) # one operation per call if result.ok: advance using result.data # e.g. the em_… id, the verified domain else: branch on the failure category # see Pattern 4 ``` With the CLI, the failure category is the exit code (`2` invalid input, `3` not found, `4` auth, `1` other), so the branch needs no message parsing: ```bash bird email get "$id" --format json > msg.json case $? in 0) jq .status msg.json ;; # advance 3) echo "wrong id — fix, don't retry" ;; 4) bird auth login ;; # recover, then re-run esac ``` The granularity is the point: an agent that can check state between steps recovers from any single failure; an agent driving one mega-operation can only start over. ## Pattern 2: A send returns 202 — the outcome arrives later `POST` a send and you get `202 Accepted` with a message ID. **Accepted means Bird took the message, not that it was delivered** — the final outcome never arrives inline. It arrives as [webhook events](/docs/guides/webhooks): `email.delivered` when the recipient's server accepts it, `email.bounced` when delivery fails permanently, `email.complained`, and so on. An agent that declares success at `202` will silently miss every bounce. Structure the task as send-then-await instead: ```text send → 202 + em_… id # record the id, do NOT report "sent successfully" await webhook event where data.email_id == em_… id: email.delivered → done email.bounced → report failure with bounce_type / bounce_description ``` Correlate on `email_id` — webhook payloads echo your tags and metadata alongside the identity fields, so your own context comes back without an extra lookup, and remember deliveries are at-least-once and unordered — deduplicate on the `webhook-id` header and sort by the payload `timestamp`. If your agent has no webhook receiver, poll the message with `GET` (or `bird email get`) until its status resolves — slower, but the same rule applies: the read-back is the source of truth, the `202` is not. ## Pattern 3: Use the sandbox as your test harness While developing the loop, don't send real mail — send to the [mail sandbox](/docs/guides/email/testing-sandbox)'s magic addresses on `messagebird.dev`. The outcome is determined by the address (`delivered@` always delivers, `bounce@` always hard-bounces, `complaint@` always complains), but everything else is the real production pipeline: the same `202`, the same event sequence, the same signed webhook deliveries, with no flag marking it as a test. ```text for address in [delivered@, bounce@, suppressed@] @messagebird.dev: send to address+run42@… # +label correlates the test case assert the expected terminal event arrives (delivered / bounced / rejected) ``` That makes the sandbox the ideal agent test harness: deterministic outcomes to assert against, zero reputation risk, no suppression-list writes, and reusable addresses across every run. An agent that passes the sandbox matrix has exercised its entire Pattern 2 path — send, await, branch — before touching a real inbox. ## Pattern 4: Recover against the standard error envelope Every Bird API error has the same shape, so error recovery is one code path, not one per endpoint: ```json { "error": { "type": "invalid_request", "code": "E01002", "name": "InvalidRequest", "message": "from address is not on a verified sending domain", "doc_url": "https://bird.com/docs/guides/errors#E01002", "request_id": "req_01krdgeqcxet5s7t44vh8rt9mg" } } ``` Each field has a job in the loop: branch on **`type`/`code`** (stable, machine-readable), show **`message`** to the human, fetch **`doc_url`** when the agent needs the docs page for that exact error — it resolves to Markdown the agent can read — and log **`request_id`** so a human can hand it to Bird support. The decisive split is retryable versus not: ```text 4xx (except 429) → a request bug: fix the input, never retry as-is 429 → back off, then retry (Pattern 5) 5xx / timeout → retry with the same Idempotency-Key (Pattern 5) ``` The full code catalog lives on the [errors page](/docs/guides/errors). With the CLI, the envelope arrives on stderr and the exit code pre-classifies it (`2`/`3`/`4`/`1` — see Pattern 1), so a shell-driving agent can branch before parsing anything. ## Pattern 5: Retry safely — Idempotency-Key, and Retry-After on 429 Retries are where agents do damage: a timeout on a send, a blind retry, and the customer gets two emails. Bird's [idempotency](/docs/guides/idempotency) support makes the retry safe — generate one `Idempotency-Key` per logical operation (not per attempt) and reuse it on every retry: ```text key = uuid() # once per logical send attempt with Idempotency-Key: key on 5xx / timeout: backoff, retry with SAME key on 2xx with Idempotency-Replay: true → the first attempt had succeeded; do not treat as a new send ``` The `Idempotency-Replay: true` response header marks a replay of the original response, so your agent can log "recovered" instead of "sent twice". The Bird SDKs inject a key automatically on every mutating request, so SDK-based agents get this for free; with the CLI, pass `--idempotency-key` on mutations that might be retried. Rate limiting gets the opposite treatment: a `429` means slow down, not try harder. The response carries a `Retry-After` header — honor it as the floor of your backoff rather than inventing your own schedule: ```text on 429: sleep max(Retry-After, backoff(attempt)); retry with the same key ``` Never retry other `4xx` responses unchanged — they're cached and replayed by idempotency precisely because the same request will always produce the same error. Fix the request (Pattern 4) and use a **new** key, since reusing a key with a different body returns `409 IdempotencyKeyReuse`. ## Next steps - [MCP server](/docs/ai/mcp-server) — the tool surface these patterns drive, hosted at `mcp.platform.bird.com` or run locally with the CLI - [CLI for agents](/docs/ai/cli-for-agents) — the same operations for shell-capable agents - [Webhooks & events](/docs/guides/webhooks) — delivery semantics, signatures, and the event catalog behind Pattern 2 - [Idempotency](/docs/guides/idempotency) — replay semantics and failure modes behind Pattern 5 - [Errors](/docs/guides/errors) — the envelope and the full error-code catalog - [Mail sandbox](/docs/guides/email/testing-sandbox) — the magic-address matrix behind Pattern 3 --- title: "CLI for agents" description: "The bird CLI's agent contract, with JSON output by default, errors on stderr, branchable exit codes, and safe retries." source: https://bird.com/en-us/docs/ai/cli-for-agents --- # CLI for agents The `bird` CLI is a single binary built to be driven by an agent. Output is JSON by default, errors go to stderr, and exit codes carry the failure category. ## Get set up The fastest path is to [set up your coding agent](/docs/ai/set-up-your-agent): paste one prompt and your agent installs the CLI and Bird's skills for you. To install it by hand: ```bash curl -fsSL https://cli.platform.bird.com/install.sh | sh bird auth login ``` `bird auth login` runs a browser OAuth flow and stores a grant bound to one workspace and its region, so there is no API key and no host to configure. `bird auth status` always exits `0` and reports `valid: true` once the token works. `bird whoami` returns the signed-in user (id, email, name) — who the agent is acting as, and the same identity the MCP `whoami` tool returns. ## The contract - JSON to stdout; errors and diagnostics to stderr; never mixed. - Exit codes: `2` invalid input, `3` not found, `4` auth or permission, `1` anything else. - Mutations take input from flags, a JSON body via `--body-file -` (stdin), or both, with the flag winning. - `--example` prints a valid request body, `--dry-run` previews the request without sending, and `--idempotency-key` makes a retry safe. ## Next steps - [Set up your coding agent](/docs/ai/set-up-your-agent): paste a prompt and your agent installs the CLI and skills. - [Agent skills](/docs/ai/agent-skills): what each skill teaches your agent. - [CLI reference](/docs/cli): every command, flag, and output shape. - [MCP server](/docs/ai/mcp-server): the same Bird surface as MCP tools, for agents that call tools instead of running a shell. --- title: "llms.txt & Markdown docs" description: "The machine-readable docs corpus: llms.txt as the index, llms-full.txt as the full concatenated corpus, and the per-page Markdown twin behind each page." source: https://bird.com/en-us/docs/ai/llms-txt --- # llms.txt & Markdown docs Everything on this docs site exists twice: once as the HTML you're reading, and once as Markdown built for machine consumption. Agents don't need a rendered page — they need clean, link-rich text they can index, retrieve from, or load whole into context. This page explains the three forms that text takes and how they relate. ## The corpus - [`/llms.txt`](/llms.txt) — the **index**: a flat map of every page with a one-line summary and a link each. This is the [llms.txt convention](https://llmstxt.org) — small enough to always fit in context, ideal as a starting point an agent follows links from. Use it when the agent retrieves on demand. - [`/llms-full.txt`](/llms-full.txt) — the **full corpus**: every doc page concatenated as Markdown in one file. Use it when you'd rather hand the agent everything up front — a long-context session, a RAG ingestion job, or a project-level knowledge file. - **The per-page Markdown twin** — every individual page is also available as raw Markdown, so an agent (or you) can grab exactly one page without the rest of the corpus. All three are generated from the same sources as the HTML site and regenerated with every deploy, so they never lag the published docs. ## The Copy-page affordance You rarely need to remember those URLs. Every page across the Guides, API reference, SDK, and knowledge-base sections carries a **Copy page** menu that hands the machine-readable forms to your tools directly: - **Copy page as Markdown** — the current page's Markdown twin, ready to paste into a chat or a context file. - **Open llms-full.txt** — the full corpus, when one page isn't enough. - **Copy MCP command** — the `bird mcp` server command from the [MCP server guide](/docs/ai/mcp-server), so the page your agent is reading can also wire up the tools to act on it. - **Connect to Cursor / Connect to VS Code** — one-click MCP setup for the two most common agentic editors. The dashboard mirrors the same affordances: the email onboarding page's **Copy for AI** menu offers the identical Markdown, corpus, and MCP shortcuts, so the path from "I'm in the dashboard" to "my agent knows Bird" is one click from either side. ## The API reference never drifts The API reference section is generated from Bird's OpenAPI specification — the same spec the API itself is validated against. Its Markdown twin is generated from that same source, which means the machine-readable reference can't drift from the live API surface: when an endpoint changes, the spec changes, and the HTML, the Markdown, and `llms-full.txt` all regenerate together. An agent reading the corpus is reading the contract, not a hand-maintained copy of it. Start at the [API reference introduction](/docs/api) to see how the reference is organized. ## Choosing a form | You want the agent to… | Hand it… | | -------------------------------------------------------- | ---------------------------------------------------------------------- | | Discover what exists and fetch pages on demand | `/llms.txt` | | Have the entire docs corpus in context or in a RAG store | `/llms-full.txt` | | Understand one topic precisely | That page's Markdown via **Copy page** | | Act on Bird, not just read about it | The [MCP server](/docs/ai/mcp-server) — docs alone can't send an email | For the hand-held, end-to-end setup — MCP server installed, editor connected, corpus wired in — follow [AI onboarding](/docs/get-started/ai-onboarding). ## Next steps - [AI onboarding](/docs/get-started/ai-onboarding) — the end-to-end setup: MCP server installed, editor connected, corpus wired in. - [MCP server](/docs/ai/mcp-server) — let your agent act on Bird instead of only reading about it. - [Agent skills](/docs/ai/agent-skills) — pre-built workflows that teach coding agents the Bird surface. --- title: "MCP server" description: "Connect any MCP client to Bird's hosted server at mcp.platform.bird.com with a URL and a browser sign-in, or run it locally over stdio with the bird CLI." source: https://bird.com/en-us/docs/ai/mcp-server --- # MCP server The Bird MCP server exposes the Bird API as [Model Context Protocol](https://modelcontextprotocol.io) tools, so any MCP-aware client — Cursor, VS Code, Claude Code, Claude Desktop, ChatGPT — can send email, manage domains, and inspect your workspace through Bird directly instead of you copy-pasting curl commands. There are two ways to run it, and most people want the first: 1. **Hosted (`mcp.platform.bird.com`)** — a URL and a browser sign-in. Nothing to install, no CLI, no API key. This is the recommended path. 2. **Local over stdio (`bird mcp`)** — the same tools running on your machine inside the [`bird` CLI](/docs/ai/cli-for-agents), for offline use, shell agents, or running it yourself. Both serve the identical curated toolset; they differ only in where the process runs and how it authenticates. ## Hosted: connect to `mcp.platform.bird.com` The hosted server speaks **Streamable HTTP** at a single endpoint: ```text https://mcp.platform.bird.com ``` Point a remote-capable MCP client at that URL and it connects — no binary to install, no token to generate. The server is stateless and regional traffic is routed automatically, so the one URL works from anywhere. ### How it authenticates There is **nothing to paste**. The hosted tier is credential-less: it stores no secrets and validates nothing itself — each request carries your own OAuth bearer token, which Bird's API validates per request and which is scoped to exactly the workspace and permissions you approved. Your client obtains that token through the standard MCP browser sign-in, and the whole handshake is automatic: 1. The client makes an unauthenticated request and gets back `401` with a `WWW-Authenticate` header pointing at Bird's [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728) protected-resource metadata (`/.well-known/oauth-protected-resource`). 2. From there it discovers the authorization server, then **registers itself dynamically** ([RFC 7591](https://www.rfc-editor.org/rfc/rfc7591)) — no pre-shared client ID, nothing for you to configure. 3. Your browser opens to a Bird consent screen. You sign in (if you aren't already), pick the workspace or organization to grant, and choose which of your permissions to delegate. Because the client registered itself, its name is self-asserted, so the screen flags it as **not verified by Bird** — confirm it's the client you actually launched before approving. 4. The client exchanges the result for an access token (PKCE; refreshed automatically) and the Bird tools appear. The granted token is capped to the intersection of what the client requested, what you approved, and what you actually hold — `org:owner` and platform-admin scopes are never delegable. The grant shows up in your profile's [**Connected apps**](https://bird.com/dashboard/profile/connected-apps) list in the dashboard, and revoking it there cuts the client off immediately. ### Connect a client The hosted server works with any client that supports remote (HTTP) MCP servers. The fastest path on Claude Code is the [bird-ai plugin](/docs/ai/agent-skills), which installs the skills **and** wires up this server in one step; the manual config for each client is below. #### Claude Code ```bash claude mcp add --transport http bird https://mcp.platform.bird.com ``` #### Cursor In `~/.cursor/mcp.json` (or **Settings → MCP**): ```json { "mcpServers": { "bird": { "url": "https://mcp.platform.bird.com" } } } ``` #### VS Code In `.vscode/mcp.json` in your project: ```json { "servers": { "bird": { "type": "http", "url": "https://mcp.platform.bird.com" } } } ``` #### Claude Desktop, ChatGPT, and other hosts Any client with a "custom connector" or "add remote MCP server" field takes the same URL — `https://mcp.platform.bird.com`. The browser sign-in runs the first time the client connects. The first time a client connects it runs the OAuth flow above; after that the Bird tools appear in the agent's tool list and the token refreshes silently. ## Local: run it over stdio with the CLI If you need an offline server, want a shell-capable agent to host it, or would rather run the process yourself, the same toolset ships inside the [`bird` CLI](/docs/ai/cli-for-agents). Install the CLI and sign in, then serve MCP over stdio: ```bash bird auth login bird mcp ``` It speaks MCP over **stdio**: your MCP client launches the process and talks to it on stdin/stdout. There is no listener to expose — the server runs on your machine, inside the client's sandbox, for exactly as long as the client needs it. The local server acts as **you**, reusing the CLI's stored login. `bird auth login` runs a browser OAuth flow where you pick one workspace and a subset of your own permissions; the issued token is capped the same way the hosted flow caps it, and `org:owner` and platform-admin scopes are never grantable. `bird mcp` reads that stored login (the same credentials file as every other `bird` command, mode `0600`) and refreshes it silently — so, as with the hosted tier, there's no `BIRD_API_KEY` and no secret in your client config. If the login is missing, `bird mcp` refuses to start and tells you to run `bird auth login`. The API host follows your login's region automatically; `--base-url` (or `BIRD_API_URL`) overrides it for testing against a non-production environment. Every MCP client takes the same two facts: the command (`bird`) and the argument (`mcp`). #### Cursor ```json { "mcpServers": { "bird": { "command": "bird", "args": ["mcp"] } } } ``` #### VS Code ```json { "servers": { "bird": { "command": "bird", "args": ["mcp"] } } } ``` #### Claude Code ```bash claude mcp add bird -- bird mcp ``` ## What the tools cover The toolset is hand-curated rather than generated from the API spec — each tool is scoped to a task an agent actually performs, and destructive operations are annotated so hosts can ask before running them: - **Send and inspect email** — `send_email`, `send_email_batch`, `list_emails`, and `get_email`, which returns the message, its per-recipient delivery statuses, and the event log in one call. - **Manage suppressions** — `list_suppressions`, `check_suppression` (is this address safe to send to?), `add_suppression`, and `remove_suppression` (annotated destructive — removing a suppression without a reason damages sender reputation). - **Set up sending domains** — `create_domain` adds a sending domain and returns the DNS records to publish; `verify_domain` re-checks them; plus `list_domains` and `get_domain`. - **Manage dedicated IPs and pools** — `create_dedicated_ip`, `list_dedicated_ips`, `get_dedicated_ip`, `assign_dedicated_ip` (move one into a pool), and `delete_dedicated_ip`; plus `create_ip_pool`, `list_ip_pools`, `get_ip_pool`, `update_ip_pool`, and `delete_ip_pool` for the pools you route sends through. - **Inspect configuration** — `list_webhooks`, `get_workspace`, and `whoami` (the signed-in user — id, email, name). Your client shows the live tool list with names, descriptions, and input schemas — that listing, not this page, is the authoritative inventory. A good first task to try end to end: > Call `whoami` to find my email, then send me a test email from onboarding@messagebird.dev and tell me when it's delivered. ## MCP or the CLI? Same surface, same auth model, different callers. For shell-capable agents (Claude Code, Cursor's terminal, CI), the [CLI](/docs/ai/cli-for-agents) is leaner: JSON output, semantic exit codes, and far fewer tokens per operation. MCP is for hosts that call tools instead of running shells — and the hosted endpoint reaches the ones that can't exec a binary at all (Claude Desktop, ChatGPT, mobile). You don't have to choose up front: the hosted URL needs no install, and the local `bird mcp` is already there once the CLI is. ## Next steps - [AI onboarding](/docs/get-started/ai-onboarding) — the quickstart version of this page, plus the machine-readable docs corpus. - [Agent skills](/docs/ai/agent-skills) — the bird-ai marketplace plugin: skills plus this MCP server, installed in one step. - [CLI for agents](/docs/ai/cli-for-agents) — drive Bird from shell-capable agents without MCP: JSON output, semantic exit codes, OAuth login. - [Authentication](/docs/guides/authentication) — API keys, regions, and how requests are authorized. --- title: "Set up your coding agent" description: "Paste one prompt and your coding agent installs the Bird CLI and Bird's skills for you. Works in Claude Code, Codex, Cursor, and any shell-capable agent." source: https://bird.com/en-us/docs/ai/set-up-your-agent --- # Set up your coding agent The fastest way to get Bird into your agent is to hand it the prompt below. Paste it once and the agent installs the [`bird` CLI](/docs/ai/cli-for-agents), signs you in, and installs Bird's [skills](/docs/ai/agent-skills), then proves it works by sending a test email. Nothing for you to run by hand. Prefer to do it yourself? The [per-client steps](#or-set-it-up-manually-per-client) are below. ## Copy this prompt into your agent ```text Bird is an infrastructure platform that makes it easy to send email to your users. Set Bird up for me, end to end. 1. Install the Bird CLI: run `curl -fsSL https://cli.platform.bird.com/install.sh | sh`. 2. Sign me in: run `bird auth login` and open the browser link for me. 3. Install Bird's skills so you know the workflows: add the bird-ai marketplace (github.com/messagebird/bird-ai) and install the "bird" plugin using this client's plugin commands. 4. Confirm who I am: run `bird whoami` — it prints the email of the account you're acting as. 5. Send me a test email so I see it land in my own inbox: use my address from `bird whoami` as the recipient, send from onboarding@messagebird.dev, then confirm delivery with `bird email get`. Subject: "Your Bird account is ready". Body: "You're set up and ready to send. This email is the proof. Cheers, The Bird team". Then let's discuss next steps, given what Bird can do. ``` The home page has per-agent buttons (Claude Code, Codex, Cursor) that each set you up with a tailored version of this prompt; the skills install differs per client (see below). ## Or set it up manually, per client Every client starts the same way; only the skills step differs. ```bash curl -fsSL https://cli.platform.bird.com/install.sh | sh # install the CLI bird auth login # sign in (browser, no API key) ``` ### Claude Code ```bash claude plugin marketplace add messagebird/bird-ai claude plugin install bird@bird-ai ``` ### Codex ```bash codex plugin marketplace add messagebird/bird-ai codex plugin add bird@bird-ai ``` ### Cursor The home-page Cursor button opens Cursor with the setup prompt prefilled (a [prompt deeplink](https://cursor.com/docs/integrations/deeplinks)); review it and send. The prompt installs Bird's skills by copying the `bird-cli` skill into `.agents/skills/`, which Cursor 2.4+ reads as a native [Agent Skill](https://agentskills.io). To do it by hand, clone github.com/messagebird/bird-ai and copy `plugins/bird/skills/bird-cli` into `.agents/skills/bird-cli`. ### Any other agent (Zed, opencode, Amp, Goose, pi, Cline, …) Copy the skill into the agent's skills directory: ```bash git clone --depth 1 https://github.com/messagebird/bird-ai /tmp/bird-ai mkdir -p .agents/skills && cp -r /tmp/bird-ai/plugins/bird/skills/bird-cli .agents/skills/bird-cli ``` Most agents read `.agents/skills/`; Cline uses `.clinerules/skills/`. This is optional: the `bird` CLI works without it. ## Authenticate `bird auth login` runs a browser OAuth flow scoped to one workspace, so there is no API key and no secret in any config. `bird auth status` reports the active workspace, region, and granted scopes. ## What the skills give your agent | Skill | What it does | | ---------------------- | ------------------------------------------------------------ | | Send and inspect email | Send a message, then confirm whether it delivered or bounced | | Sending domains | Find a verified sender, or run the create-and-verify loop | | Webhook endpoints | Register, test, inspect, and delete outbound endpoints | ## Connect the MCP server instead If your client calls tools instead of running a shell (Claude Desktop, ChatGPT, mobile), point it at Bird's hosted MCP server at `https://mcp.platform.bird.com`. The full reference, including the local-stdio option, is in the [MCP server guide](/docs/ai/mcp-server). ## Next steps - [CLI for agents](/docs/ai/cli-for-agents): the `bird` CLI's agent contract. - [Agent skills](/docs/ai/agent-skills): what each skill teaches, and the per-client support matrix. - [MCP server](/docs/ai/mcp-server): the hosted endpoint and tool surface. --- title: "Introduction" description: "Orientation for the Bird API reference — how it is organized, the conventions every endpoint follows, and where to start." source: https://bird.com/en-us/docs/api --- # Introduction The Bird API is a unified REST API for everything the platform does. This reference documents every public endpoint, generated from the same OpenAPI specification that drives the official SDKs, so the request and response shapes here are exactly what the wire carries. The reference is grouped by resource into three sections: - **Channels** — sending and message-level resources: email messages, broadcasts, suppressions, and per-channel configuration. - **Products** — product surfaces built on the channels, such as templates and analytics. - **Platform** — the resources that exist regardless of channel: workspaces, API keys, sending domains, dedicated IPs, IP pools, and webhooks. Resource pages are deep-linked from the [guides](/docs): when a guide mentions an endpoint, the link lands on its reference entry here. ## Conventions Every endpoint follows the same conventions. They are stated once here rather than repeated on each page. - **Base path** — all endpoints live under `/v1` on a regional host such as `https://us1.platform.bird.com`. See [Base URLs & regions](/docs/api/regions). - **Authentication** — requests carry an API key as a bearer token: `Authorization: Bearer bk_us1_...`. See [Authentication](/docs/api/authentication). - **JSON, snake_case** — request and response bodies are JSON with `snake_case` field names (`created_at`, `workspace_id`), and requests must set `Content-Type: application/json`. - **Timestamps** — all timestamps are RFC 3339 strings in UTC, in fields suffixed `_at` (`created_at`, `revoked_at`). Timestamp fields are server-assigned and read-only. - **Typed resource IDs** — every ID carries a type prefix: `em_` for email messages, `dom_` for sending domains, `whk_` for webhook endpoints, `sup_` for suppressions, and so on. The prefix makes an ID self-describing in logs and prevents passing one resource's ID where another's is expected. - **Partial updates use PATCH** — the API never uses `PUT`. A `PATCH` request changes only the fields you include; omitted fields are left untouched. - **Errors** — every error response carries the same envelope, with a `type` for coarse branching, a stable `code`, a human-readable `message`, and the `request_id` to quote when contacting support. See [Errors](/docs/api/errors). - **Pagination** — list endpoints use cursor-based pagination with a shared parameter set. See [Pagination](/docs/api/pagination). - **Idempotency** — mutating endpoints accept an `Idempotency-Key` header so retries are safe. See [Idempotency](/docs/api/idempotency). ## Recommended clients You can call the API with any HTTP client, but the official clients handle authentication, region selection, retries, and pagination for you: - The official SDKs for [TypeScript](/docs/sdks/typescript), Go, and Python — typed methods over the curated public surface - The [Bird CLI](/docs/cli) — the API from your terminal, also suited to scripts and agents ## Read next - [Authentication](/docs/api/authentication) — how requests authenticate at the wire level - [Base URLs & regions](/docs/api/regions) — regional hosts and the region model - [Pagination](/docs/api/pagination) — cursors, page sizes, and sorting - [Idempotency](/docs/api/idempotency) — safe retries for mutating requests - [Errors](/docs/api/errors) — the error envelope and the full error catalog --- title: "Authentication" description: "Wire-level authentication for the Bird API — the bearer header, API key anatomy, and the exact failure responses." source: https://bird.com/en-us/docs/api/authentication --- # Authentication Every API request authenticates with an API key passed as a bearer token in the `Authorization` header: ```bash curl https://us1.platform.bird.com/v1/email/messages \ -H "Authorization: Bearer bk_us1_Ab3xKq9mP2wR5tY8uI1oL4nJ..." ``` Keys are workspace-scoped: a key authenticates as its workspace, carries the scopes chosen at creation, and cannot reach resources in any other workspace. How to create, scope, rotate, and revoke keys is covered in the [Authentication & API keys guide](/docs/guides/authentication) (keys live under [**Developers → API keys**](https://bird.com/dashboard/w/api-keys)); this page covers the wire-level contract. ## Key format ```text bk_us1_Ab3xKq9mP2wR5tY8uI1oL4nJ... └┬┘└┬┘ └──────────┬──────────┘└┬┘ │ │ payload checksum │ └ region (routes the request) └ Bird key prefix ``` A key is `bk_{region}_{payload}{checksum}`: - **`bk_{region}_`** — the prefix identifies the credential type and the region the key was created in. `bk_us1_` keys are valid only against `https://us1.platform.bird.com`, `bk_eu1_` keys only against `https://eu1.platform.bird.com` — the prefix is what official SDKs and the CLI use to pick the host. The fixed `bk_` prefix is registered with GitHub secret scanning, so a Bird key leaked in a public repository is detected and reported. - **Payload** — a long random string with 128+ bits of entropy. - **Checksum** — the final 6 characters are a checksum of the rest of the key, letting a client reject a mistyped or truncated key locally before any request is made. The full key is returned exactly once, in the response that creates it. Bird stores only an HMAC-SHA-256 hash — the plaintext cannot be retrieved again, and the dashboard shows only a short `key_prefix` (the first 12 characters). A lost key is revoked and replaced, never recovered. ## Failure responses All failures use the standard [error envelope](/docs/api/errors). | Status | When | | ------ | ------------------------------------------------------------------------------------------------------------- | | `401` | The `Authorization` header is missing, the key is malformed or unknown, or the key has been revoked. | | `403` | The key is valid but lacks the scope the endpoint requires. | | `421` | The key's region does not match the host — a `bk_eu1_...` key sent to `us1.platform.bird.com`, or vice versa. | The `421 Misdirected Request` body (error type `misdirected_error`, code `E01010`) names the correct regional host, so a client can detect the mistake and re-send without guesswork. See [Base URLs & regions](/docs/api/regions). ## Dashboard sessions are not API keys The Bird dashboard does not use API keys: a person logging in gets a session cookie, scoped to their own user permissions. Session cookies are not accepted on the programmatic API surface, and API keys are not accepted by the dashboard. Server workloads always use API keys. ## Related - [Authentication & API keys guide](/docs/guides/authentication) — creating, scoping, rotating, and revoking keys - [Base URLs & regions](/docs/api/regions) — regional hosts and the region model - [Errors](/docs/api/errors) — the error envelope and catalog --- title: "Errors" description: "The error wire contract — the envelope fields, HTTP status mapping, validation details, and how each SDK surfaces failures." source: https://bird.com/en-us/docs/api/errors --- # Errors Every failed request returns the same JSON envelope under a top-level `error` key, with an HTTP status that tells you the coarse category. This page is the wire contract; for guidance on branching, retries, and the code catalog philosophy, see [Errors](/docs/guides/errors). ```json { "error": { "type": "validation_error", "code": "E01001", "name": "ValidationError", "message": "Request validation failed.", "doc_url": "https://bird.com/docs/api/errors/E01001", "request_id": "req_01krdgeqcxet5s7t44vh8rt9mg", "details": [ { "param": "contact_id", "message": "this field is reserved and not yet supported" }, { "param": "topic_id", "message": "this field is reserved and not yet supported" } ] } } ``` ## Envelope fields | Field | Always present | Description | | ------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | Yes | Broad category for coarse branching — a closed enum (`auth_error`, `validation_error`, `rate_limit_error`, ...). | | `code` | Yes | Opaque, stable identifier matching `E\d{5}`. Unique, never renamed, never reused — the canonical thing to match on. | | `name` | Yes | Human-readable slug (`ValidationError`) for log readability. Always paired with `code`, never a replacement for it. | | `message` | Yes | Human-readable description. Not stable — display or log it, never parse it. | | `doc_url` | Yes | Stable link to the documentation page for this `code`. | | `request_id` | Yes | Correlation ID, also returned as the `X-Request-Id` response header. Quote it in support requests. | | `param` | No | The offending field, when a single field is at fault. | | `details` | No | Per-field validation failures as `{param, message}` objects, where `param` is a dotted path like `to[0].email`. Present only on `validation_error` responses. | | `vendor_code` | No | Verbatim code from a downstream system (an SMTP reply code, a payment decline code) when one is worth acting on. | ## HTTP status mapping Each `type` maps to exactly one HTTP status, so the status and the envelope never disagree. | Status | `type` | Meaning | | ------ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | `bad_request_error` | The request was malformed — unparseable body, invalid header (for example, a bad `Idempotency-Key`). | | `401` | `auth_error` | Missing, invalid, or revoked credentials. See [Authentication](/docs/api/authentication). | | `402` | `billing_error` | The request requires a payment method, balance, or plan the organization does not have. | | `403` | `permission_error` | Authenticated, but not allowed — insufficient API key scope or workspace role. | | `404` | `not_found_error` | No such route, or no such resource in this workspace. | | `409` | `conflict_error` | The request conflicts with current state — including idempotency conflicts `E01004` and `E01005` ([Idempotency](/docs/api/idempotency)). | | `412` | `precondition_error` | A precondition for the request was not met. | | `413` | `payload_too_large_error` | The request body exceeds the maximum allowed size. | | `421` | `misdirected_error` | The request was routed to the wrong region — use the regional host that matches your key's `bk_{region}_` prefix. | | `422` | `validation_error` | The body parsed but the values are invalid. `details` lists each failing field; reserved-but-unshipped fields such as `contact_id` and `topic_id` on email sends also land here (`E04007`). | | `429` | `rate_limit_error` | A [rate-limit group](/docs/guides/rate-limits) is exhausted. `Retry-After` gives the wait in seconds, and the `RateLimit`/`RateLimit-Policy` headers name the group and quota. | | `500` | `internal_error` | Bird-side failure. Safe to retry with backoff — and with the same idempotency key. | | `501` | `not_implemented_error` | The endpoint is declared but not yet implemented. | | `503` | `service_unavailable_error` | A dependency is temporarily unavailable. Retry with backoff. | ## Handling errors in the SDKs Each [SDK](/docs/sdks/concepts) maps the envelope onto its language's native error model and carries every envelope field (`type`, `code`, `message`, `doc_url`, `request_id`, ...) on the error value. ```typescript import { BirdRateLimitError, BirdValidationError, BirdAPIError } from "@messagebird/sdk"; try { await bird.email.send({ from: { email: "onboarding@messagebird.dev", name: "Bird" }, to: ["delivered@messagebird.dev"], subject: "Hello from Bird", html: "

My first Bird email.

", }); } catch (err) { if (err instanceof BirdRateLimitError) console.log(`rate limited — retry in ${err.retryAfter}s`); else if (err instanceof BirdValidationError) console.error(err.details); else if (err instanceof BirdAPIError) console.error(err.code, err.requestId); else throw err; } ``` ```go 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 } } ``` ```python from bird import APIStatusError, RateLimitError, ValidationError try: client.email.send( from_={"email": "onboarding@messagebird.dev", "name": "Bird"}, to=["delivered@messagebird.dev"], subject="Hello from Bird", text="My first Bird email.", ) except RateLimitError as err: print("rate limited; retry after", err.retry_after) except ValidationError as err: print(err.status_code, err.details) except APIStatusError as err: print(err.status_code, err.code, err.request_id) ``` ## Related - [Errors concepts](/docs/guides/errors) — branching strategy, validation details, and `vendor_code` semantics - [Authentication](/docs/api/authentication) — credentials behind `401` and `403` - [Idempotency](/docs/api/idempotency) — the `409` conflict errors and safe retries - [Rate limits](/docs/guides/rate-limits) — groups, headers, and handling `429` --- title: "Idempotency" description: "The Idempotency-Key wire contract — header format, replay header, conflict errors, retention, and SDK auto-keying." source: https://bird.com/en-us/docs/api/idempotency --- # Idempotency The Bird API supports opt-in request deduplication via the `Idempotency-Key` header. This page is the wire contract; for the concepts — why, when, and retry strategy — see [Idempotency](/docs/guides/idempotency). ## Request header | Header | Constraints | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Idempotency-Key` | Optional. Any non-empty string up to 255 characters; a UUID v4 is recommended. Honored on `POST`, `PATCH`, and `DELETE`; ignored on `GET`, `HEAD`, and `OPTIONS`. | Omitting the header skips idempotency entirely — the request is processed normally with no deduplication. An empty key or one longer than 255 characters returns `400` with code `E01002 InvalidRequest`. Keys are scoped to your workspace (two workspaces can use the same key string without collision) and retained for roughly **3 hours**. After the retention window, a reused key is processed as a fresh request. ```bash curl -X POST https://us1.platform.bird.com/v1/email/messages \ -H "Authorization: Bearer bk_us1_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \ -d '{ "from": "hello@example.com", "to": ["user@example.com"], "subject": "Welcome!", "html": "

Hi.

" }' ``` ## Response semantics | Scenario | Response | | ------------------------------------------ | ---------------------------------------------------------------------------------------- | | First request with a key | Processed normally; the response (`2xx` or `4xx`) is cached against the key. | | Same key, identical request | Original status and body replayed, with the `Idempotency-Replay: true` response header. | | Same key, **different** request | `409` — `E01005 IdempotencyKeyReuse`. Generate a new key for the new request. | | Same key, original request still in flight | `409` — `E01004 RequestInProgress`. The lock expires within ~30 seconds; wait and retry. | | Original request returned `5xx` | Not cached — the key unlocks and the retry is processed fresh. | A replayed response is byte-for-byte the original — same status code, same body — distinguished only by the extra header: ```http HTTP/1.1 202 Accepted Idempotency-Replay: true ``` "Identical request" covers the method, endpoint, path and query parameters, and the raw request body — any difference, including whitespace, makes it a different request and triggers `E01005`. Both `409` errors come wrapped in the standard [error envelope](/docs/api/errors). `5xx` responses are deliberately never cached: a server error means Bird does not know whether the request took effect, and replaying it would permanently block the retry from succeeding. Retry `5xx` and timeouts with the **same** key. ## SDK behavior The official [SDKs](/docs/sdks/concepts) attach an auto-generated UUID `Idempotency-Key` to every mutating request, generated once per logical call and reused across all retry attempts of that call. You can supply your own key per call (`idempotencyKey` in TypeScript, `option.WithIdempotencyKey` in Go, `idempotency_key` in Python) when one logical operation spans multiple SDK calls. To detect a replay, read the `Idempotency-Replay` response header through each SDK's transport-metadata accessor: `.withResponse()` in TypeScript, `option.WithResponseInto` in Go, and `with_raw_response` in Python. ## Related - [Idempotency concepts](/docs/guides/idempotency) — retry strategy, key design, and the durable email-send guarantee - [Errors](/docs/api/errors) — the envelope wrapping `E01004` and `E01005` - [Email messages](/docs/api/reference/create-email-message) — the send endpoint, the most common place to use a key - [SDK concepts](/docs/sdks/concepts) — automatic key generation and retries --- title: "Pagination" description: "The cursor pagination wire contract — limit, starting_after, ending_before, opaque cursors, opt-in totals, and how the SDKs iterate for you." source: https://bird.com/en-us/docs/api/pagination --- # Pagination Every paginated list endpoint in the Bird API uses the same cursor-based contract: the same request parameters, the same response envelope, the same cursor semantics. Learn it once on [`GET /v1/email/messages`](/docs/api/reference/list-email-messages) and it applies everywhere. A small number of bounded collections (for example, billing plans) return a plain `{"data": [...]}` array with no pagination fields at all — an endpoint either implements this contract in full or not at all, never partially. ## Request parameters | Parameter | Type | Description | | ---------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `limit` | integer | Maximum items per page. Between `1` and `100`; defaults to `25`. | | `starting_after` | string | Cursor from the `next_cursor` field of a previous response. Returns items immediately after that position. | | `ending_before` | string | Cursor from the `prev_cursor` field of a previous response. Returns items immediately before that position. | | `include_total` | boolean | When `true`, the response includes a `total` count. Defaults to `false`. Available on management endpoints only — high-volume data endpoints (messages, events, suppressions) do not accept it. | Cursors are **opaque**: they are not resource IDs, they encode the sort position internally, and their format can change at any time. Receive them in responses and pass them back unchanged. A malformed or expired cursor returns a `422` with code `E01012 InvalidCursor` — restart pagination without a cursor. Most list endpoints also accept resource-specific `sort` and `order` parameters; the per-endpoint reference documents the allowed sort fields. Changing the sort invalidates cursors from the previous sort order. ## Response envelope ```json { "data": [{ "...": "..." }], "next_cursor": "WyIyMDI2LTA2LTEwVDA5OjE0OjAzWiIsICJtc2dfMDFr...", "prev_cursor": null, "refresh_cursor": "WyIyMDI2LTA2LTEwVDEyOjAwOjAwWiIsICJtc2dfMDFr...", "total": 1432 } ``` | Field | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `data` | The page of items. | | `next_cursor` | Pass back as `starting_after` to fetch the next page. `null` when no next page exists — this is the loop-termination signal. | | `prev_cursor` | Pass back as `ending_before` to step backward. `null` when no previous page exists (always `null` on the first page). | | `refresh_cursor` | A refresh anchor: save it, then pass it back as `ending_before` later to fetch items that have appeared since this response. Non-null whenever `data` is non-empty. | | `total` | Total items matching the request's filters across all pages. Present only when `include_total=true` was passed; otherwise `null`/absent. | `next_cursor` and `prev_cursor` are independent — each is `null` exactly when its direction has no further page. There is no separate `has_more` flag; check `next_cursor` for `null`. ## Walking all pages with curl ```bash CURSOR="" while :; do RESP=$(curl -s "https://us1.platform.bird.com/v1/email/messages?limit=100${CURSOR:+&starting_after=$CURSOR}" \ -H "Authorization: Bearer bk_us1_...") echo "$RESP" | jq -r '.data[].id' CURSOR=$(echo "$RESP" | jq -r '.next_cursor // empty') [ -z "$CURSOR" ] && break done ``` The first request carries no cursor; every subsequent request passes the previous response's `next_cursor` as `starting_after`; the loop ends when `next_cursor` is `null`. ## Pagination in the SDKs You rarely write that loop by hand. Every [Bird SDK](/docs/sdks/concepts) exposes list endpoints in two modes: lazy iteration that fetches pages transparently as you consume items, and a single-page accessor for manual cursor control. ```typescript for await (const message of bird.email.list({ status: "bounced" })) { console.log(message.id); } ``` ```go for msg, err := range client.Email.List(context.Background(), bird.EmailListParams{Status: bird.EmailStatusBounced}) { if err != nil { log.Fatal(err) } fmt.Println(msg.Id) } ``` ```python for message in client.email.list(status="delivered"): print(message.id) ``` ## Rate limits List endpoints share the `list` [rate-limit group](/docs/guides/rate-limits), separate from reads of individual resources and from writes — paginating through a large collection never consumes your send or management quota. The lazy iterators above issue one request per page, so a tight loop over a large collection draws down the `list` group at one request per `limit` items; use `limit=100` for bulk reads. ## Related - [Email messages](/docs/api/reference/list-email-messages) — a representative paginated list endpoint - [SDK concepts](/docs/sdks/concepts) — iteration and single-page accessors across the SDKs - [Rate limits](/docs/guides/rate-limits) — groups, headers, and handling `429`s --- title: "Base URLs & regions" description: "Regional base URLs, the organization-level region model, and how requests are routed to the right region." source: https://bird.com/en-us/docs/api/regions --- # Base URLs & regions The Bird API is served from regional hosts. There is no single global data-plane URL — each request goes directly to the region that holds your organization's data. | Base URL | Serves | | ------------------------------- | ------------------------------------------------------------ | | `https://us1.platform.bird.com` | All `/v1` data-plane endpoints for organizations in `us1` | | `https://eu1.platform.bird.com` | All `/v1` data-plane endpoints for organizations in `eu1` | | `https://platform.bird.com` | Region-independent surfaces only: `/v1/auth` and `/v1/admin` | Region codes are two lowercase letters for the geography plus an instance digit (`^[a-z]{2}[0-9]+$`). `us1` and `eu1` are live today; the scheme accommodates future regions such as `us2` and `ap1` without any client change. ## Organizations are pinned to a region Every organization is assigned a region at signup — auto-detected from your location, overridable before you confirm — and the assignment is immutable in v1. All of the organization's workspaces, API keys, messages, recipient data, and event logs live in that one region and are never replicated across regions, which is what makes data-residency commitments (such as GDPR-driven EU residency) provable: an EU organization's data is stored and processed in the EU, full stop. See the [data residency article](/docs/knowledge-base/compliance/consent-and-privacy) for the compliance details. Only authentication and account administration (`/v1/auth`, `/v1/admin`) operate on globally replicated data, which is why they are served from the non-region host `platform.bird.com`. ## Routing: the key selects the host An [API key](/docs/api/authentication) encodes its region in its prefix: `bk_us1_...` keys belong to `us1`, `bk_eu1_...` keys to `eu1`. Send each key to its matching regional host. A request that reaches the wrong region is not forwarded — it is rejected with `421 Misdirected Request`, and the error message names the correct host: ```json { "type": "misdirected_error", "code": "E01010", "name": "MisdirectedRequest", "message": "Organization belongs to region eu1 but this request reached us1. Send the request to eu1.platform.bird.com.", "request_id": "..." } ``` Every API response also carries an `X-Bird-Region` header naming the region that served it, useful when confirming which region a request actually landed in. ## SDKs and the CLI route automatically The official SDKs and the CLI parse the region out of the key prefix and pick the host for you — with a `bk_eu1_...` key, every call goes to `eu1.platform.bird.com` without configuration. You can override the inferred host when needed (for example, against a test environment): `baseUrl` in the TypeScript SDK, `option.WithBaseURL` in the Go SDK, and `--region` on the CLI. ## Related - [Authentication](/docs/api/authentication) — key format and failure responses - [Data residency & privacy](/docs/knowledge-base/compliance/consent-and-privacy) — the compliance view of region pinning --- title: "Bird CLI" description: "The bird command-line interface — operate the Bird API from a shell, a script, or an AI agent with JSON output, semantic exit codes, and safe retries." source: https://bird.com/en-us/docs/cli --- # Bird CLI `bird` is the Bird API as a command line: one binary that sends email, manages sending domains, and configures webhooks. It is built for two callers at once — a human at a terminal, and an agent or script driving it in a loop. Every command emits JSON to stdout by default, writes errors as a structured envelope on stderr, and exits with a semantic code, so a consumer branches on structure rather than parsing prose. ## Install Homebrew (macOS, Linux): ```bash brew install messagebird/tap/bird ``` Or the install script (macOS, Linux): ```bash curl -fsSL https://cli.platform.bird.com/install.sh | sh ``` The script detects your platform, verifies the download, and prints where the binary landed. Pass `--version ` to pin a release or `--install-dir ` to choose the destination. On Windows, use the PowerShell installer at `https://cli.platform.bird.com/install.ps1` instead. Verify with `bird version`. ## Authenticate ```bash bird auth login ``` This opens a browser consent page where you pick a workspace and the permissions to grant. The CLI stores a workspace-bound OAuth token in `~/.config/bird/credentials.json` and refreshes it automatically on use — there is no API key to create or copy around, and the API region is recorded from the workspace so there's no host to configure. On a headless machine or over SSH, `bird auth login --device` prints a code you approve on another device instead of opening a local browser. Check that the credential works: ```bash bird auth status ``` `auth status` reports whether a token is configured and whether it validates against the API, plus the workspace, region, and granted scopes. It always exits `0` — branch on the `valid` field in its JSON output. Pass `--offline` to skip the API call, and `bird auth logout` to discard the stored credential. ## First commands Send an email and read it back by ID: ```bash bird email send --from onboarding@messagebird.dev --to delivered@messagebird.dev --subject "Hello" --html "

Hi from the CLI.

" bird email get em_019c1930687b7bfa8a1b2c3d4e5f6789 ``` The [CLI quickstart](/docs/get-started/quickstarts/cli/email) walks this flow end to end — including Bird's shared onboarding domain and sandbox address, so you can send before verifying a domain of your own. Mutations take input three ways, and the inline value wins: flags, a JSON body on stdin, or both — so one piped template serves many calls (`cat body.json | bird email send --to x@y.com`). Two flags make every write safe to rehearse and retry: - `--dry-run` prints the resolved request body that would be sent and exits without sending — the verification gate before anything outbound. - `--idempotency-key ` makes a retry safe: the server replays the original response for any duplicate request with the same key, the same idempotency mechanism the [SDKs](/docs/sdks/typescript) use, so a network timeout never means a double send. Write commands also support `--example`, which prints a complete, valid request body (generated from the API schema, no credentials needed) and exits. Destructive commands (`delete`) require an explicit ID and `--yes`, so a loose retry can't quietly destroy state. ## Output contract Data goes to stdout as JSON with no flag needed; diagnostics and errors go to stderr, never mixed with data. Lists return a cursor envelope (`{"data": [...], "next_cursor": ...}`) with a default `--limit`, so output is always bounded — pipe to `jq` to extract fields (`bird email list | jq -r '.data[].id'`). On single-record reads (`get`, `show`, `status`), `--format text` (`-f text`) opts into a human-readable card instead. Failures are a JSON envelope on stderr with machine-branchable fields — `code` (stable ID), `type`, `retryable` and `retry_after`, `param` and `details` for the offending input, and `next_actions` listing runnable `bird` commands to recover. API errors pass the server's error code, request ID, and docs link straight through. See [Errors](/docs/guides/errors) for the underlying API error model. Exit codes are semantic, so a script or agent branches without reading any text: | Exit code | Meaning | | --------- | ------------------------------------------------------- | | `0` | Success. | | `1` | Unexpected / unrecognized error. Surface and stop. | | `2` | Invalid flags, arguments, or body. | | `3` | Resource not found. | | `4` | Authentication or authorization failure. | | `5` | Conflict or failed precondition. | | `6` | Rate limit or server error — retry after `retry_after`. | Commands never prompt interactively, so an agent or CI job can't hang on a question it didn't ask for; missing input fails fast with exit `2` and an actionable hint. The one blocking command is `bird auth login`, which waits for browser or device approval and then times out. ## Configuration ```bash bird config show ``` `config show` prints the resolved configuration — the API base URL and where it came from, plus the config, cache, and state paths. The base URL resolves in order: the `--base-url` global flag, the `BIRD_API_URL` environment variable, then the region recorded with your login (`{region}.platform.bird.com`) — so after `bird auth login` there is normally nothing to set. The CLI follows XDG paths (`~/.config/bird`, `~/.cache/bird`, `~/.local/state/bird`); set `BIRD_CONFIG_DIR` to collapse all three under one root, useful for isolated CI or agent sandboxes. Two global flags work on every command: - `--format` (`-f`) — `json` (default) or `text` (single-record reads only). - `--base-url` — override the API endpoint for one invocation, equivalent to `BIRD_API_URL`. ## Discover the surface ```bash bird commands ``` This prints the entire command tree as JSON — every command, its purpose, its flags with types and defaults, required positionals, and the error contract — so an agent enumerates the full surface in one call instead of scraping `--help`. The working loop is: `bird commands` to see what exists, `--example` or `--help` for the shape, `--dry-run` to preview, then run with `--idempotency-key` for safe retries. Shell completion is available via `bird completion bash|zsh|fish`. ## Command groups - [`bird auth`](/docs/cli#authenticate) — `login`, `status`, `logout`: manage the OAuth credential. - [`bird email`](/docs/get-started/quickstarts/cli/email) — `send`, `get`, `list`: send messages and track their delivery status. - [`bird email domains`](/docs/guides/email/sending-domains) — `create`, `get`, `list`, `verify`: register sending domains and check DNS verification. - [`bird email inbound-addresses`](/docs/guides/email/receiving-email) — `create`, `get`, `list`, `update`, `delete`: mint and manage the forwarding addresses Bird receives mail at. - [`bird email inbound-messages`](/docs/guides/email/receiving-email) — `list`, `get`, `body`, `attachments`: read the mail Bird received. - [`bird webhooks`](/docs/guides/webhooks) — `create`, `get`, `list`, `test`, `delete`: manage webhook endpoints and fire test deliveries. ## Next steps - [CLI quickstart](/docs/get-started/quickstarts/cli/email) — install, log in, and send your first email in two minutes. - [The CLI for agents](/docs/ai/cli-for-agents) — the full agent contract: JSON output, exit codes, `--dry-run`, error envelope, and discovery. - [SDKs](/docs/sdks/typescript) — the same API surface as typed libraries for TypeScript, Go, and Python. --- title: "AI onboarding" description: "Point your IDE or coding agent at Bird: paste one setup prompt to install the bird CLI and skills, connect the MCP server, or load the docs corpus." source: https://bird.com/en-us/docs/get-started/ai-onboarding --- # AI onboarding Bird is built to be driven by agents as much as by humans. The fastest path is to hand your coding agent a **setup prompt**: paste it once and the agent installs the [`bird` CLI](/docs/ai/cli-for-agents) and Bird's [skills](/docs/ai/agent-skills), then proves it works by sending a test email. The CLI is the default because it is the leanest agent surface: JSON output, semantic exit codes, far fewer tokens per operation than tool calls, and a self-describing command tree. If your client calls tools instead of running a shell, [connect the MCP server](#2-connect-the-mcp-server-when-it-fits) instead. Set it up once and your agent can send, inspect, and debug on Bird without you copy-pasting anything. ## 1. Set up your agent (CLI + skills) The fastest path is to [set up your coding agent](/docs/ai/set-up-your-agent): paste one prompt and your agent installs the [`bird` CLI](/docs/ai/cli-for-agents) and Bird's [skills](/docs/ai/agent-skills), signs you in, and sends a test email to prove it works. On the home page (and the Products and Email pages), the **Claude Code / Codex / Cursor** buttons copy the prompt tailored to each one. The skills teach the agent Bird's workflows (send and confirm delivery, find a verified sender, manage webhook endpoints) so it reaches a correct command on the first try. ## 2. Connect the MCP server (when it fits) If your client calls tools instead of running a shell (Claude Desktop, ChatGPT developer mode, mobile), point it at Bird's hosted MCP server instead. It is a URL and a browser sign-in, nothing to install: ```text https://mcp.platform.bird.com ``` The first time a client connects, your browser opens a Bird consent screen: you sign in, pick the workspace and the permissions to grant, and approve. There is no API key to create and no secret in your client config. The endpoint, the full tool surface, the delegated OAuth model, and the local-stdio option (`bird mcp`) are in the [MCP server guide](/docs/ai/mcp-server). ## 3. Feed your agent the docs corpus Everything on this docs site is also published in machine-readable form, so agents that prefer reading over tool calling get the same content you're looking at now: - [`/llms.txt`](/llms.txt) — the index: a flat, link-rich summary of every page, ideal as a starting point the agent can follow links from. - [`/llms-full.txt`](/llms-full.txt) — the full corpus: every doc page concatenated as Markdown, for agents that want the whole thing in context. Both are regenerated with the site, so they never drift from the live API surface. Point your agent at the index URL, paste the full corpus into a long-context session, or use the **Copy page** menu on any individual page — it copies that page as Markdown and links to both corpus files. The dashboard's [**onboarding**](https://bird.com/dashboard/w/email/onboarding) page mirrors the same Copy-for-AI shortcuts. ![The email onboarding page in the Bird dashboard, with the API-key step, a runnable send snippet, and the Copy for AI action](/images/docs/dashboard-email-onboarding.png) The formats, sizing, and retrieval tips are covered in the [llms.txt guide](/docs/ai/llms-txt). ## 4. Try it Once your agent is set up, give it a real task end to end: > Find my email with whoami, then send me a test email from onboarding@messagebird.dev and tell me when it's delivered. The agent reads your address with `whoami`, sends from Bird's shared onboarding domain — which delivers to verified members of your workspace, so your own inbox works with no domain to verify — and polls the status until it lands, so the proof shows up in your inbox. That's the same happy path as the [send-your-first-email quickstart](/docs/get-started/send-your-first-email), just driven by the agent. (For a deterministic check with no real inbox, send to `delivered@messagebird.dev` instead — see the [testing sandbox](/docs/guides/email/testing-sandbox).) ## Next steps - [CLI for agents](/docs/ai/cli-for-agents) — the `bird` CLI's agent contract: JSON output, semantic exit codes, OAuth login. The default surface. - [Agent skills](/docs/ai/agent-skills) — the bird-ai marketplace plugin and what each client gets. - [MCP server](/docs/ai/mcp-server) — the hosted endpoint, the full tool surface, the delegated OAuth model, and the local-stdio option. - [llms.txt & Markdown docs](/docs/ai/llms-txt) — how the machine-readable corpus is built and how to retrieve from it. - [Send your first email](/docs/get-started/send-your-first-email) — the human-driven version of the happy path above. --- title: "Quickstarts" description: "Pick your language or framework and go from an API key to a delivered email in a few minutes." source: https://bird.com/en-us/docs/get-started/quickstarts --- # Quickstarts Each quickstart is one hand-held happy path: install the SDK, set your API key, and send your first email — no domain setup needed, using Bird's shared onboarding sender. Pick the one that matches your stack. ## TypeScript / Node.js - [Node.js](/docs/get-started/quickstarts/typescript/email) — a plain script with `@messagebird/sdk`. - Frameworks: [Next.js](/docs/get-started/quickstarts/typescript/next-js/email), [Remix](/docs/get-started/quickstarts/typescript/remix/email), [Nuxt](/docs/get-started/quickstarts/typescript/nuxt/email), [SvelteKit](/docs/get-started/quickstarts/typescript/sveltekit/email), [Express](/docs/get-started/quickstarts/typescript/express/email), [Hono](/docs/get-started/quickstarts/typescript/hono/email), [Bun](/docs/get-started/quickstarts/typescript/bun/email), and [Astro](/docs/get-started/quickstarts/typescript/astro/email). ## Python - [Python](/docs/get-started/quickstarts/python/email) — a plain script with the Bird Python SDK. - Frameworks: [FastAPI](/docs/get-started/quickstarts/python/fastapi/email), [Django](/docs/get-started/quickstarts/python/django/email), and [Flask](/docs/get-started/quickstarts/python/flask/email). ## Go - [Go](/docs/get-started/quickstarts/go/email) — a standard-library HTTP handler. - [Gin](/docs/get-started/quickstarts/go/gin/email). ## No SDK - [cURL](/docs/get-started/quickstarts/curl/email) — the raw HTTP request, paste it into any terminal. - [CLI](/docs/get-started/quickstarts/cli/email) — send from your shell with the `bird` CLI. ## Next steps Want the concepts behind the happy path? [Send your first email](/docs/get-started/send-your-first-email) walks through API keys, regions, and what happens after the `202`. Building with an AI assistant? See [AI onboarding](/docs/get-started/ai-onboarding). --- title: "CLI" description: "Send your first email from the terminal with the bird CLI — log in once, then send and check messages with two commands." source: https://bird.com/en-us/docs/get-started/quickstarts/cli/email --- # CLI Send your first email straight from the terminal: install the `bird` CLI, log in once, and send. No code, no API key to copy around. ## 1. Install Homebrew (macOS, Linux): ```bash brew install messagebird/tap/bird ``` Or the install script (macOS, Linux): ```bash curl -fsSL https://cli.platform.bird.com/install.sh | sh ``` On Windows, use the PowerShell installer at `https://cli.platform.bird.com/install.ps1`. See [Install the CLI](/docs/cli#install) for details. ## 2. Log in ```bash bird auth login ``` This opens a browser consent page where you pick a workspace and the permissions to grant. The CLI stores a workspace-bound OAuth token in `~/.config/bird/credentials.json` and refreshes it automatically on use — the API region comes from the workspace, so there's no host or key to configure. On a headless machine or over SSH, use `bird auth login --device` to get a code you approve on another device. ## 3. Send an email Send from Bird's shared onboarding domain to the `delivered@messagebird.dev` sandbox address — no domain verification, no real mailbox needed: ```bash bird email send \ --from onboarding@messagebird.dev \ --to delivered@messagebird.dev \ --subject "Hello from Bird" \ --html "

My first Bird email.

" ``` The CLI prints the accepted message as JSON: ```json { "id": "em_019c1930687b7bfa8a1b2c3d4e5f6789", "status": "accepted", "category": "transactional", "to": [{ "email": "delivered@messagebird.dev" }], "created_at": "2026-06-10T14:30:00Z" } ``` `--to` is repeatable for multiple recipients, `--text` sends a plain-text body, and `--dry-run` prints the resolved request without sending it. ## 4. Check the result `accepted` means Bird took the message — read it back by its `em_` ID and watch the status reach `delivered`: ```bash bird email get em_019c1930687b7bfa8a1b2c3d4e5f6789 ``` Add `--format text` for a human-readable card instead of JSON. Everything you just did is script- and agent-friendly by design: every command emits JSON to stdout by default, errors are a JSON envelope on stderr, and exit codes are semantic — so a script (or an AI agent) branches on structure, never on prose. See [the CLI for agents](/docs/ai/cli-for-agents) for the full contract. ## Next steps - [CLI reference](/docs/cli) — every command, flag, and output shape. - [The CLI for agents](/docs/ai/cli-for-agents) — JSON output, exit codes, `--dry-run`, and idempotent retries. - [Send your first email](/docs/get-started/send-your-first-email) — the same flow over raw HTTP and the SDKs. - [Sending domains](/docs/guides/email/sending-domains) — verify your own domain for production sending. --- title: "cURL" description: "Send your first email with nothing but curl — raw HTTP requests against the Bird email API, no SDK required." source: https://bird.com/en-us/docs/get-started/quickstarts/curl/email --- # cURL Send your first email with nothing but curl: export an API key, POST a message, and GET it back to watch it deliver. This is the raw HTTP flow — everything the SDKs do starts here. ## 1. Export your API key Create a key in the dashboard under **Developers → API keys** — the [hero quickstart](/docs/get-started/send-your-first-email) walks through it. Then export it: ```bash export BIRD_API_KEY="bk_us1_..." ``` The region prefix picks your API host: `bk_us1_` keys call `https://us1.platform.bird.com`, `bk_eu1_` keys call `https://eu1.platform.bird.com`. The snippets below use `us1` — swap the host if your key says otherwise. ## 2. Send an email POST to `/v1/email/messages`, sending from Bird's shared onboarding domain to the `delivered@messagebird.dev` sandbox address — no domain verification, no real mailbox needed: ```bash curl -X POST https://us1.platform.bird.com/v1/email/messages \ -H "Authorization: Bearer $BIRD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "onboarding@messagebird.dev", "to": ["delivered@messagebird.dev"], "subject": "Hello from Bird", "html": "

My first Bird email.

" }' ``` The API responds with `202` immediately — Bird has accepted the email and delivers it asynchronously: ```json { "id": "em_019c1930687b7bfa8a1b2c3d4e5f6789", "status": "accepted", "category": "transactional", "to": ["delivered@messagebird.dev"], "recipient_count": 1, "created_at": "2026-06-10T14:30:00Z" } ``` Unlike the SDKs, raw HTTP doesn't generate an idempotency key for you — if you retry a POST after a timeout, add an `Idempotency-Key: ` header so the retry replays the original result instead of sending twice. ## 3. Watch it deliver GET the message by its `em_` ID and watch the status move from `accepted` through `processed` to `delivered`: ```bash curl https://us1.platform.bird.com/v1/email/messages/em_019c1930687b7bfa8a1b2c3d4e5f6789 \ -H "Authorization: Bearer $BIRD_API_KEY" ``` Because the recipient is the `delivered@messagebird.dev` sandbox address, delivery is guaranteed — the message flows through Bird's real pipeline but never touches a real mailbox. ## Next steps - [Email API reference](/docs/api/reference/create-email-message) — the full request and response schema. - [Authentication](/docs/api/authentication) — keys, scopes, and the `Authorization` header. - [Send your first email](/docs/get-started/send-your-first-email) — the same flow with the dashboard and SDKs. - [Sending domains](/docs/guides/email/sending-domains) — verify your own domain for production sending. --- title: "Go" description: "Send your first email from a Go application with the Bird Go SDK in three steps." source: https://bird.com/en-us/docs/get-started/quickstarts/go/email --- # Go Send your first email from Go in three steps: install the SDK, write a handler that sends, and run it. This quickstart uses the standard library's `net/http` — no framework required. ## 1. Install ```bash go get github.com/messagebird/bird-sdk-go ``` The SDK requires Go 1.24+. Grab an API key from **Developers → API keys** in the dashboard and export it — the region is inferred from the key's `bk_us1_` / `bk_eu1_` prefix, so there's nothing else to configure: ```bash export BIRD_API_KEY="bk_us1_..." ``` ## 2. Send Create `main.go` with an HTTP handler that sends through Bird's shared onboarding domain — no domain verification needed: ```go package main import ( "encoding/json" "errors" "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"))) if err != nil { log.Fatal(err) } http.HandleFunc("POST /send", func(w http.ResponseWriter, r *http.Request) { msg, err := client.Email.Send(r.Context(), bird.EmailSendParams{ From: "onboarding@messagebird.dev", To: []string{"delivered@messagebird.dev"}, Subject: "Hello from Bird", HTML: "

My first Bird email.

", }) if err != nil { var apiErr *bird.APIError if errors.As(err, &apiErr) { http.Error(w, apiErr.Error(), apiErr.StatusCode) return } http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusAccepted) _ = json.NewEncoder(w).Encode(msg) }) log.Fatal(http.ListenAndServe(":3000", nil)) } ``` Every `Send` is automatically idempotent: the SDK generates one idempotency key per call and reuses it across retries, so transient failures (timeouts, 429s, 5xx) are retried without ever double-delivering. ## 3. Try it ```bash go run . & curl -X POST localhost:3000/send ``` The API responds with `202` — Bird has accepted the email and delivers it asynchronously. The handler above writes back the full message object; the key fields are the `em_` ID and the status: ```json { "id": "em_01krdgeqcxet5s7t44vh8rt9mg", "status": "accepted" } ``` Because the recipient is the `delivered@messagebird.dev` sandbox address, delivery is guaranteed — fetch the message by its `em_` ID with `client.Email.Get` and watch the status move to `delivered`. ## Next steps - [Go SDK reference](/docs/sdks/go) — every method, param, and error type. - [Send your first email](/docs/get-started/send-your-first-email) — the same flow with curl and the dashboard. - [Sending domains](/docs/guides/email/sending-domains) — verify your own domain for production sending. - [Email API reference](/docs/api/reference/create-email-message) — the full request and response schema. --- title: "Go · Gin" description: "Send your first email from a Gin application with the Bird Go SDK in three steps." source: https://bird.com/en-us/docs/get-started/quickstarts/go/gin/email --- # Go · Gin Send your first email from a [Gin](https://gin-gonic.com) application in three steps: install the SDK, add a route that sends, and run it. ## 1. Install ```bash go get github.com/messagebird/bird-sdk-go ``` The SDK requires Go 1.24+. Grab an API key from **Developers → API keys** in the dashboard and export it — the region is inferred from the key's `bk_us1_` / `bk_eu1_` prefix, so there's nothing else to configure: ```bash export BIRD_API_KEY="bk_us1_..." ``` ## 2. Send Create `main.go` with a route that sends through Bird's shared onboarding domain — no domain verification needed: ```go package main import ( "errors" "log" "net/http" "os" "github.com/gin-gonic/gin" 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) } r := gin.Default() r.POST("/send", func(c *gin.Context) { msg, err := client.Email.Send(c.Request.Context(), bird.EmailSendParams{ From: "onboarding@messagebird.dev", To: []string{"delivered@messagebird.dev"}, Subject: "Hello from Bird", HTML: "

My first Bird email.

", }) if err != nil { var apiErr *bird.APIError if errors.As(err, &apiErr) { c.JSON(apiErr.StatusCode, gin.H{"error": apiErr.Error()}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusAccepted, msg) }) log.Fatal(r.Run(":3000")) } ``` Every `Send` is automatically idempotent: the SDK generates one idempotency key per call and reuses it across retries, so transient failures (timeouts, 429s, 5xx) are retried without ever double-delivering. ## 3. Try it ```bash go run . & curl -X POST localhost:3000/send ``` The API responds with `202` — Bird has accepted the email and delivers it asynchronously: ```json { "id": "em_019c1930687b7bfa8a1b2c3d4e5f6789", "status": "accepted" } ``` Because the recipient is the `delivered@messagebird.dev` sandbox address, delivery is guaranteed — fetch the message by its `em_` ID with `client.Email.Get` and watch the status move to `delivered`. ## Next steps - [Go SDK reference](/docs/sdks/go) — every method, param, and error type. - [Send your first email](/docs/get-started/send-your-first-email) — the same flow with curl and the dashboard. - [Sending domains](/docs/guides/email/sending-domains) — verify your own domain for production sending. - [Email API reference](/docs/api/reference/create-email-message) — the full request and response schema. --- title: "Python · Django" description: "Send your first email from a Django view with the Bird Python SDK." source: https://bird.com/en-us/docs/get-started/quickstarts/python/django/email --- # Python · Django Send your first email from a Django view using the Bird Python SDK's sync client. Three steps: install, send, run. ## 1. Install the SDK ```bash pip install messagebird-sdk django ``` ```bash # or uv add messagebird-sdk django poetry add messagebird-sdk django ``` Requires Python 3.10+. The import package is `bird`. ## 2. Send an email Export your API key — the SDK reads `BIRD_API_KEY` from the environment and infers the region (`us1` or `eu1`) from the `bk_us1_` / `bk_eu1_` prefix, so `Bird()` needs no arguments: ```bash export BIRD_API_KEY="bk_us1_..." ``` Add a view, e.g. in `myapp/views.py`. Construct one `Bird` at module load and reuse it across requests — it pools connections and is safe to share across threads: ```python from bird import Bird, APIError from django.http import JsonResponse client = Bird() def send_email(request): try: message = client.email.send( from_="onboarding@messagebird.dev", to=["delivered@messagebird.dev"], subject="Hello from Bird", html="

My first Bird email.

", ) except APIError as err: return JsonResponse({"error": str(err)}, status=502) return JsonResponse({"id": message.id, "status": message.status}, status=202) ``` Wire it up in `urls.py`: ```python from django.urls import path from myapp.views import send_email urlpatterns = [path("send/", send_email)] ``` `from_` is the Python spelling of the `from` field (`from` is a reserved word). `onboarding@messagebird.dev` is Bird's shared onboarding sender — no domain verification needed — and `delivered@messagebird.dev` is a sandbox recipient that always delivers. Transient failures retry automatically, and a retried send reuses one idempotency key across attempts, so it never double-applies. ## 3. Try it ```bash python manage.py runserver curl http://localhost:8000/send/ ``` Bird accepts the message with a `202` and delivers it asynchronously; your view returns the `em_` id and status: ```json { "id": "em_019c1930687b7bfa8a1b2c3d4e5f6789", "status": "accepted" } ``` Fetch the message by its `em_` id with `client.email.get(...)` and watch the status move from `accepted` to `delivered`. ## Next steps - [Python SDK email reference](/docs/sdks/python) — every method and option. - [Send your first email](/docs/get-started/send-your-first-email) — the same flow with curl and the full sandbox address list. - [Sending domains](/docs/guides/email/sending-domains) — verify your own domain for production sending. - [Email API reference](/docs/api/reference/create-email-message) — the full request and response schema. --- title: "Python" description: "Send your first email with the Bird Python SDK in a plain script." source: https://bird.com/en-us/docs/get-started/quickstarts/python/email --- # Python Send your first email from a plain Python script using the Bird Python SDK's sync client. Three steps: install, send, run. ## 1. Install the SDK ```bash pip install messagebird-sdk ``` ```bash # or uv add messagebird-sdk poetry add messagebird-sdk ``` Requires Python 3.10+. The import package is `bird`. ## 2. Send an email Export your API key — the SDK reads `BIRD_API_KEY` from the environment and infers the region (`us1` or `eu1`) from the `bk_us1_` / `bk_eu1_` prefix, so `Bird()` needs no arguments: ```bash export BIRD_API_KEY="bk_us1_..." ``` Create `send.py`: ```python from bird import APIError, Bird with Bird() as client: try: message = client.email.send( from_={"email": "onboarding@messagebird.dev", "name": "Bird"}, to=["delivered@messagebird.dev"], subject="Hello from Bird", html="

My first Bird email.

", ) print(message.id, message.status) except APIError as err: print("send failed:", err) ``` `from_` is the Python spelling of the `from` field (`from` is a reserved word). `onboarding@messagebird.dev` is Bird's shared onboarding sender — no domain verification needed — and `delivered@messagebird.dev` is a sandbox recipient that always delivers. Transient failures retry automatically, and a retried send reuses one idempotency key across attempts, so it never double-applies. ## 3. Run it ```bash python send.py ``` The API responds with `202` — the message is accepted and delivered asynchronously: ```json { "id": "em_019c1930687b7bfa8a1b2c3d4e5f6789", "status": "accepted" } ``` Fetch the message by its `em_` id with `client.email.get(message.id)` and watch the status move from `accepted` to `delivered`. ## Next steps - [Python SDK reference](/docs/sdks/python) — every method and option. - [Send your first email](/docs/get-started/send-your-first-email) — the same flow with curl and the full sandbox address list. - [Sending domains](/docs/guides/email/sending-domains) — verify your own domain for production sending. - [Email API reference](/docs/api/reference/create-email-message) — the full request and response schema. --- title: "Python · FastAPI" description: "Send your first email from an async FastAPI endpoint with the Bird Python SDK." source: https://bird.com/en-us/docs/get-started/quickstarts/python/fastapi/email --- # Python · FastAPI Send your first email from a FastAPI endpoint using the Bird Python SDK's async client. Three steps: install, send, run. ## 1. Install the SDK ```bash pip install messagebird-sdk "fastapi[standard]" ``` ```bash # or uv add messagebird-sdk "fastapi[standard]" poetry add messagebird-sdk "fastapi[standard]" ``` Requires Python 3.10+. The import package is `bird`. ## 2. Send an email Export your API key — the SDK reads `BIRD_API_KEY` from the environment and infers the region (`us1` or `eu1`) from the `bk_us1_` / `bk_eu1_` prefix, so `AsyncBird()` needs no arguments: ```bash export BIRD_API_KEY="bk_us1_..." ``` Create `main.py`. Construct one `AsyncBird` at startup and reuse it across requests — it pools connections and is safe to share across tasks: ```python from bird import AsyncBird, APIError from fastapi import FastAPI, HTTPException app = FastAPI() client = AsyncBird() @app.post("/send", status_code=202) async def send() -> dict[str, str]: try: message = await client.email.send( from_="onboarding@messagebird.dev", to=["delivered@messagebird.dev"], subject="Hello from Bird", html="

My first Bird email.

", ) except APIError as err: raise HTTPException(status_code=502, detail=str(err)) return {"id": message.id, "status": message.status} ``` `from_` is the Python spelling of the `from` field (`from` is a reserved word). `onboarding@messagebird.dev` is Bird's shared onboarding sender — no domain verification needed — and `delivered@messagebird.dev` is a sandbox recipient that always delivers. Transient failures retry automatically, and a retried send reuses one idempotency key across attempts, so it never double-applies. ## 3. Try it ```bash fastapi dev main.py curl -X POST http://localhost:8000/send ``` Bird accepts the message with a `202` and delivers it asynchronously; your endpoint returns the `em_` id and status: ```json { "id": "em_019c1930687b7bfa8a1b2c3d4e5f6789", "status": "accepted" } ``` Fetch the message by its `em_` id with `await client.email.get(...)` and watch the status move from `accepted` to `delivered`. ## Next steps - [Python SDK email reference](/docs/sdks/python) — every method and option. - [Send your first email](/docs/get-started/send-your-first-email) — the same flow with curl and the full sandbox address list. - [Sending domains](/docs/guides/email/sending-domains) — verify your own domain for production sending. - [Email API reference](/docs/api/reference/create-email-message) — the full request and response schema. --- title: "Python · Flask" description: "Send your first email from a Flask route with the Bird Python SDK." source: https://bird.com/en-us/docs/get-started/quickstarts/python/flask/email --- # Python · Flask Send your first email from a Flask route using the Bird Python SDK's sync client. Three steps: install, send, run. ## 1. Install the SDK ```bash pip install messagebird-sdk flask ``` ```bash # or uv add messagebird-sdk flask poetry add messagebird-sdk flask ``` Requires Python 3.10+. The import package is `bird`. ## 2. Send an email Export your API key — the SDK reads `BIRD_API_KEY` from the environment and infers the region (`us1` or `eu1`) from the `bk_us1_` / `bk_eu1_` prefix, so `Bird()` needs no arguments: ```bash export BIRD_API_KEY="bk_us1_..." ``` Create `app.py`. Construct one `Bird` at module load and reuse it across requests — it pools connections and is safe to share across threads: ```python from bird import Bird, APIError from flask import Flask, jsonify app = Flask(__name__) client = Bird() @app.post("/send") def send(): try: message = client.email.send( from_="onboarding@messagebird.dev", to=["delivered@messagebird.dev"], subject="Hello from Bird", html="

My first Bird email.

", ) except APIError as err: return jsonify({"error": str(err)}), 502 return jsonify({"id": message.id, "status": message.status}), 202 ``` `from_` is the Python spelling of the `from` field (`from` is a reserved word). `onboarding@messagebird.dev` is Bird's shared onboarding sender — no domain verification needed — and `delivered@messagebird.dev` is a sandbox recipient that always delivers. Transient failures retry automatically, and a retried send reuses one idempotency key across attempts, so it never double-applies. ## 3. Try it ```bash flask run curl -X POST http://localhost:5000/send ``` Bird accepts the message with a `202` and delivers it asynchronously; your route returns the `em_` id and status: ```json { "id": "em_019c1930687b7bfa8a1b2c3d4e5f6789", "status": "accepted" } ``` Fetch the message by its `em_` id with `client.email.get(...)` and watch the status move from `accepted` to `delivered`. ## Next steps - [Python SDK email reference](/docs/sdks/python) — every method and option. - [Send your first email](/docs/get-started/send-your-first-email) — the same flow with curl and the full sandbox address list. - [Sending domains](/docs/guides/email/sending-domains) — verify your own domain for production sending. - [Email API reference](/docs/api/reference/create-email-message) — the full request and response schema. --- title: "TypeScript · Astro" description: "Send your first email with the Bird TypeScript SDK from an Astro API route." source: https://bird.com/en-us/docs/get-started/quickstarts/typescript/astro/email --- # TypeScript · Astro Send an email from an Astro API route with `@messagebird/sdk`. You'll need a Bird API key — create one in **Developers → API keys** as covered in [Send your first email](/docs/get-started/send-your-first-email), then put it in `.env` as `BIRD_API_KEY`. ## 1. Install ```bash npm create astro@latest my-app && cd my-app npm install @messagebird/sdk # pnpm add @messagebird/sdk # yarn add @messagebird/sdk # bun add @messagebird/sdk ``` ## 2. Send Create `src/pages/api/send.ts` — `prerender = false` makes this route render on the server, on demand: ```typescript import type { APIRoute } from "astro"; import { BirdClient } from "@messagebird/sdk"; export const prerender = false; const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! }); export const POST: APIRoute = async () => { const msg = await bird.email.send({ from: "onboarding@messagebird.dev", to: ["delivered@messagebird.dev"], subject: "Hello from Bird", html: "

My first Bird email.

", }); console.log(msg.id, msg.status); // "em_…", "accepted" return Response.json({ id: msg.id, status: msg.status }); }; ``` `onboarding@messagebird.dev` is Bird's shared onboarding sender and `delivered@messagebird.dev` is a sandbox recipient that always delivers — no domain verification needed. The region is inferred from your key's `bk_us1_` / `bk_eu1_` prefix, so there's no base URL to configure. The SDK auto-generates an `Idempotency-Key` per call, so retried sends are safe. ## 3. Try it Run the dev server and POST to the route: ```bash npm run dev curl -X POST http://localhost:4321/api/send ``` The API responds with `202` — Bird has accepted the message and delivers it asynchronously: ```json { "id": "em_019c1930687b7bfa8a1b2c3d4e5f6789", "status": "accepted" } ``` To deploy on-demand routes to production, add a server adapter (`@astrojs/node`, `@astrojs/vercel`, …) — static-only builds can't run them. ## Next steps - [TypeScript email SDK](/docs/sdks/typescript) — the full email surface: `send`, `get`, `list`, and the error model. - [Send your first email](/docs/get-started/send-your-first-email) — creating an API key and the full set of sandbox addresses. - [Sending domains](/docs/guides/email/sending-domains) — verify your own domain for production sending. - [Email API reference](/docs/api/reference/create-email-message) — the full request and response schema. --- title: "TypeScript · Bun" description: "Send your first email with the Bird TypeScript SDK from a Bun.serve fetch handler." source: https://bird.com/en-us/docs/get-started/quickstarts/typescript/bun/email --- # TypeScript · Bun Send an email from a `Bun.serve` fetch handler with `@messagebird/sdk` — Bun runs TypeScript directly, no build step. You'll need a Bird API key — create one in **Developers → API keys** as covered in [Send your first email](/docs/get-started/send-your-first-email), then put it in `.env` as `BIRD_API_KEY` (Bun loads `.env` automatically). ## 1. Install ```bash bun init -y bun add @messagebird/sdk # npm install @messagebird/sdk # pnpm add @messagebird/sdk # yarn add @messagebird/sdk ``` ## 2. Send Create `server.ts`: ```typescript import { BirdClient } from "@messagebird/sdk"; const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! }); Bun.serve({ port: 3000, async fetch(req) { if (req.method !== "POST") return new Response("Not found", { status: 404 }); const msg = await bird.email.send({ from: "onboarding@messagebird.dev", to: ["delivered@messagebird.dev"], subject: "Hello from Bird", html: "

My first Bird email.

", }); console.log(msg.id, msg.status); // "em_…", "accepted" return Response.json({ id: msg.id, status: msg.status }); }, }); ``` `onboarding@messagebird.dev` is Bird's shared onboarding sender and `delivered@messagebird.dev` is a sandbox recipient that always delivers — no domain verification needed. The region is inferred from your key's `bk_us1_` / `bk_eu1_` prefix, so there's no base URL to configure. The SDK auto-generates an `Idempotency-Key` per call, so retried sends are safe. ## 3. Try it Run the server and POST to it: ```bash bun run server.ts curl -X POST http://localhost:3000/ ``` The API responds with `202` — Bird has accepted the message and delivers it asynchronously: ```json { "id": "em_019c1930687b7bfa8a1b2c3d4e5f6789", "status": "accepted" } ``` ## Next steps - [TypeScript email SDK](/docs/sdks/typescript) — the full email surface: `send`, `get`, `list`, and the error model. - [Send your first email](/docs/get-started/send-your-first-email) — creating an API key and the full set of sandbox addresses. - [Sending domains](/docs/guides/email/sending-domains) — verify your own domain for production sending. - [Email API reference](/docs/api/reference/create-email-message) — the full request and response schema. --- title: "TypeScript / Node.js" description: "Send your first email with the Bird TypeScript SDK from a plain Node.js script." source: https://bird.com/en-us/docs/get-started/quickstarts/typescript/email --- # TypeScript / Node.js Send an email from a plain Node.js script with `@messagebird/sdk`. You'll need a Bird API key — create one in **Developers → API keys** as covered in [Send your first email](/docs/get-started/send-your-first-email), then export it as `BIRD_API_KEY`. ## 1. Install ```bash npm install @messagebird/sdk # pnpm add @messagebird/sdk # yarn add @messagebird/sdk # bun add @messagebird/sdk ``` The SDK requires Node.js 20.3+ and is ESM-only. ## 2. Send Create `send.ts`: ```typescript import { BirdClient } from "@messagebird/sdk"; const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! }); const msg = await bird.email.send({ from: { email: "onboarding@messagebird.dev", name: "Bird" }, to: ["delivered@messagebird.dev"], subject: "Hello from Bird", html: "

My first Bird email.

", }); console.log(msg.id, msg.status); ``` `onboarding@messagebird.dev` is Bird's shared onboarding sender and `delivered@messagebird.dev` is a sandbox recipient that always delivers — no domain verification needed. The region is inferred from your key's `bk_us1_` / `bk_eu1_` prefix, so there's no base URL to configure. The SDK auto-generates an `Idempotency-Key` per call, so retried sends are safe. ## 3. Run it ```bash npx tsx send.ts ``` The API responds with `202` — Bird has accepted the message and delivers it asynchronously: ```json { "id": "em_01krdgeqcxet5s7t44vh8rt9mg", "status": "accepted" } ``` ## Next steps - [TypeScript email SDK](/docs/sdks/typescript) — the full email surface: `send`, `get`, `list`, and the error model. - [Send your first email](/docs/get-started/send-your-first-email) — creating an API key and the full set of sandbox addresses. - [Sending domains](/docs/guides/email/sending-domains) — verify your own domain for production sending. - [Email API reference](/docs/api/reference/create-email-message) — the full request and response schema. --- title: "TypeScript · Express" description: "Send your first email with the Bird TypeScript SDK from an Express 5 route." source: https://bird.com/en-us/docs/get-started/quickstarts/typescript/express/email --- # TypeScript · Express Send an email from an Express 5 POST route with `@messagebird/sdk`. You'll need a Bird API key — create one in **Developers → API keys** as covered in [Send your first email](/docs/get-started/send-your-first-email), then export it as `BIRD_API_KEY`. ## 1. Install ```bash npm install express @messagebird/sdk # pnpm add express @messagebird/sdk # yarn add express @messagebird/sdk # bun add express @messagebird/sdk ``` ## 2. Send Create `src/server.ts` — Express 5 forwards rejections from async handlers to the error middleware, so no try/catch wrapper is needed: ```typescript import express from "express"; import { BirdClient } from "@messagebird/sdk"; const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! }); const app = express(); app.post("/send", async (req, res) => { const msg = await bird.email.send({ from: "onboarding@messagebird.dev", to: ["delivered@messagebird.dev"], subject: "Hello from Bird", html: "

My first Bird email.

", }); console.log(msg.id, msg.status); // "em_…", "accepted" res.json({ id: msg.id, status: msg.status }); }); app.listen(3000); ``` `onboarding@messagebird.dev` is Bird's shared onboarding sender and `delivered@messagebird.dev` is a sandbox recipient that always delivers — no domain verification needed. The region is inferred from your key's `bk_us1_` / `bk_eu1_` prefix, so there's no base URL to configure. The SDK auto-generates an `Idempotency-Key` per call, so retried sends are safe. ## 3. Try it Run the server and POST to the route: ```bash npx tsx src/server.ts curl -X POST http://localhost:3000/send ``` The API responds with `202` — Bird has accepted the message and delivers it asynchronously: ```json { "id": "em_019c1930687b7bfa8a1b2c3d4e5f6789", "status": "accepted" } ``` ## Next steps - [TypeScript email SDK](/docs/sdks/typescript) — the full email surface: `send`, `get`, `list`, and the error model. - [Send your first email](/docs/get-started/send-your-first-email) — creating an API key and the full set of sandbox addresses. - [Sending domains](/docs/guides/email/sending-domains) — verify your own domain for production sending. - [Email API reference](/docs/api/reference/create-email-message) — the full request and response schema. --- title: "TypeScript · Hono" description: "Send your first email with the Bird TypeScript SDK from a Hono POST handler." source: https://bird.com/en-us/docs/get-started/quickstarts/typescript/hono/email --- # TypeScript · Hono Send an email from a Hono POST handler with `@messagebird/sdk`. Both are edge-ready, so this works on Node, Bun, Deno, and Cloudflare Workers alike. You'll need a Bird API key — create one in **Developers → API keys** as covered in [Send your first email](/docs/get-started/send-your-first-email), then export it as `BIRD_API_KEY`. ## 1. Install ```bash npm create hono@latest my-app && cd my-app npm install @messagebird/sdk # pnpm add @messagebird/sdk # yarn add @messagebird/sdk # bun add @messagebird/sdk ``` ## 2. Send Add the route in `src/index.ts`: ```typescript import { Hono } from "hono"; import { BirdClient } from "@messagebird/sdk"; const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! }); const app = new Hono(); app.post("/send", async (c) => { const msg = await bird.email.send({ from: "onboarding@messagebird.dev", to: ["delivered@messagebird.dev"], subject: "Hello from Bird", html: "

My first Bird email.

", }); console.log(msg.id, msg.status); // "em_…", "accepted" return c.json({ id: msg.id, status: msg.status }); }); export default app; ``` `onboarding@messagebird.dev` is Bird's shared onboarding sender and `delivered@messagebird.dev` is a sandbox recipient that always delivers — no domain verification needed. The region is inferred from your key's `bk_us1_` / `bk_eu1_` prefix, so there's no base URL to configure. The SDK auto-generates an `Idempotency-Key` per call, so retried sends are safe. ## 3. Try it Run the dev server and POST to the route: ```bash npm run dev curl -X POST http://localhost:3000/send ``` The API responds with `202` — Bird has accepted the message and delivers it asynchronously: ```json { "id": "em_019c1930687b7bfa8a1b2c3d4e5f6789", "status": "accepted" } ``` ## Next steps - [TypeScript email SDK](/docs/sdks/typescript) — the full email surface: `send`, `get`, `list`, and the error model. - [Send your first email](/docs/get-started/send-your-first-email) — creating an API key and the full set of sandbox addresses. - [Sending domains](/docs/guides/email/sending-domains) — verify your own domain for production sending. - [Email API reference](/docs/api/reference/create-email-message) — the full request and response schema. --- title: "TypeScript · Next.js" description: "Send your first email with the Bird TypeScript SDK from a Next.js Server Action." source: https://bird.com/en-us/docs/get-started/quickstarts/typescript/next-js/email --- # TypeScript · Next.js Send an email from a Next.js App Router Server Action with `@messagebird/sdk`. You'll need a Bird API key — create one in **Developers → API keys** as covered in [Send your first email](/docs/get-started/send-your-first-email), then put it in `.env.local` as `BIRD_API_KEY`. ## 1. Install ```bash npx create-next-app@latest my-app && cd my-app npm install @messagebird/sdk # pnpm add @messagebird/sdk # yarn add @messagebird/sdk # bun add @messagebird/sdk ``` ## 2. Send Create `app/actions/send-welcome.ts` — the `"use server"` directive keeps it (and your API key) on the server: ```typescript "use server"; import { BirdClient } from "@messagebird/sdk"; const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! }); export async function sendWelcome() { const msg = await bird.email.send({ from: "onboarding@messagebird.dev", to: ["delivered@messagebird.dev"], subject: "Hello from Bird", html: "

My first Bird email.

", }); console.log(msg.id, msg.status); // "em_…", "accepted" return { id: msg.id, status: msg.status }; } ``` `onboarding@messagebird.dev` is Bird's shared onboarding sender and `delivered@messagebird.dev` is a sandbox recipient that always delivers — no domain verification needed. The region is inferred from your key's `bk_us1_` / `bk_eu1_` prefix, so there's no base URL to configure. The SDK auto-generates an `Idempotency-Key` per call, so retried sends are safe. ## 3. Try it Wire the action to a form in `app/page.tsx` and click the button: ```typescript import { sendWelcome } from "./actions/send-welcome"; export default function Home() { return (
); } ``` Run `npm run dev`, open `http://localhost:3000`, and submit. The API responds with `202` — Bird has accepted the message and delivers it asynchronously: ```json { "id": "em_019c1930687b7bfa8a1b2c3d4e5f6789", "status": "accepted" } ``` ## Next steps - [TypeScript email SDK](/docs/sdks/typescript) — the full email surface: `send`, `get`, `list`, and the error model. - [Send your first email](/docs/get-started/send-your-first-email) — creating an API key and the full set of sandbox addresses. - [Sending domains](/docs/guides/email/sending-domains) — verify your own domain for production sending. - [Email API reference](/docs/api/reference/create-email-message) — the full request and response schema. --- title: "TypeScript · Nuxt" description: "Send your first email with the Bird TypeScript SDK from a Nuxt server route." source: https://bird.com/en-us/docs/get-started/quickstarts/typescript/nuxt/email --- # TypeScript · Nuxt Send an email from a Nuxt server route (a Nitro event handler) with `@messagebird/sdk`. You'll need a Bird API key — create one in **Developers → API keys** as covered in [Send your first email](/docs/get-started/send-your-first-email), then put it in `.env` as `BIRD_API_KEY`. ## 1. Install ```bash npm create nuxt@latest my-app && cd my-app npm install @messagebird/sdk # pnpm add @messagebird/sdk # yarn add @messagebird/sdk # bun add @messagebird/sdk ``` ## 2. Send Create `server/api/send.post.ts` — everything under `server/` runs only on the server: ```typescript import { BirdClient } from "@messagebird/sdk"; const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! }); export default defineEventHandler(async () => { const msg = await bird.email.send({ from: "onboarding@messagebird.dev", to: ["delivered@messagebird.dev"], subject: "Hello from Bird", html: "

My first Bird email.

", }); console.log(msg.id, msg.status); // "em_…", "accepted" return { id: msg.id, status: msg.status }; }); ``` `onboarding@messagebird.dev` is Bird's shared onboarding sender and `delivered@messagebird.dev` is a sandbox recipient that always delivers — no domain verification needed. The region is inferred from your key's `bk_us1_` / `bk_eu1_` prefix, so there's no base URL to configure. The SDK auto-generates an `Idempotency-Key` per call, so retried sends are safe. ## 3. Try it Run the dev server and POST to the route: ```bash npm run dev curl -X POST http://localhost:3000/api/send ``` The API responds with `202` — Bird has accepted the message and delivers it asynchronously: ```json { "id": "em_019c1930687b7bfa8a1b2c3d4e5f6789", "status": "accepted" } ``` ## Next steps - [TypeScript email SDK](/docs/sdks/typescript) — the full email surface: `send`, `get`, `list`, and the error model. - [Send your first email](/docs/get-started/send-your-first-email) — creating an API key and the full set of sandbox addresses. - [Sending domains](/docs/guides/email/sending-domains) — verify your own domain for production sending. - [Email API reference](/docs/api/reference/create-email-message) — the full request and response schema. --- title: "TypeScript · Remix" description: "Send your first email with the Bird TypeScript SDK from a Remix route action." source: https://bird.com/en-us/docs/get-started/quickstarts/typescript/remix/email --- # TypeScript · Remix Send an email from a Remix route action with `@messagebird/sdk`. You'll need a Bird API key — create one in **Developers → API keys** as covered in [Send your first email](/docs/get-started/send-your-first-email), then put it in `.env` as `BIRD_API_KEY`. ## 1. Install ```bash npx create-remix@latest my-app && cd my-app npm install @messagebird/sdk # pnpm add @messagebird/sdk # yarn add @messagebird/sdk # bun add @messagebird/sdk ``` ## 2. Send Create `app/routes/send.ts` — a resource route whose `action` runs only on the server: ```typescript import { BirdClient } from "@messagebird/sdk"; const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! }); export async function action() { const msg = await bird.email.send({ from: "onboarding@messagebird.dev", to: ["delivered@messagebird.dev"], subject: "Hello from Bird", html: "

My first Bird email.

", }); console.log(msg.id, msg.status); // "em_…", "accepted" return Response.json({ id: msg.id, status: msg.status }); } ``` `onboarding@messagebird.dev` is Bird's shared onboarding sender and `delivered@messagebird.dev` is a sandbox recipient that always delivers — no domain verification needed. The region is inferred from your key's `bk_us1_` / `bk_eu1_` prefix, so there's no base URL to configure. The SDK auto-generates an `Idempotency-Key` per call, so retried sends are safe. ## 3. Try it Run the dev server and POST to the route: ```bash npm run dev curl -X POST http://localhost:5173/send ``` The API responds with `202` — Bird has accepted the message and delivers it asynchronously: ```json { "id": "em_019c1930687b7bfa8a1b2c3d4e5f6789", "status": "accepted" } ``` ## Next steps - [TypeScript email SDK](/docs/sdks/typescript) — the full email surface: `send`, `get`, `list`, and the error model. - [Send your first email](/docs/get-started/send-your-first-email) — creating an API key and the full set of sandbox addresses. - [Sending domains](/docs/guides/email/sending-domains) — verify your own domain for production sending. - [Email API reference](/docs/api/reference/create-email-message) — the full request and response schema. --- title: "TypeScript · SvelteKit" description: "Send your first email with the Bird TypeScript SDK from a SvelteKit server endpoint." source: https://bird.com/en-us/docs/get-started/quickstarts/typescript/sveltekit/email --- # TypeScript · SvelteKit Send an email from a SvelteKit `+server.ts` endpoint with `@messagebird/sdk`. You'll need a Bird API key — create one in **Developers → API keys** as covered in [Send your first email](/docs/get-started/send-your-first-email), then put it in `.env` as `BIRD_API_KEY`. ## 1. Install ```bash npx sv create my-app && cd my-app npm install @messagebird/sdk # pnpm add @messagebird/sdk # yarn add @messagebird/sdk # bun add @messagebird/sdk ``` ## 2. Send Create `src/routes/api/send/+server.ts` — `+server.ts` files run only on the server: ```typescript import { json } from "@sveltejs/kit"; import { BirdClient } from "@messagebird/sdk"; const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! }); export async function POST() { const msg = await bird.email.send({ from: "onboarding@messagebird.dev", to: ["delivered@messagebird.dev"], subject: "Hello from Bird", html: "

My first Bird email.

", }); console.log(msg.id, msg.status); // "em_…", "accepted" return json({ id: msg.id, status: msg.status }); } ``` `onboarding@messagebird.dev` is Bird's shared onboarding sender and `delivered@messagebird.dev` is a sandbox recipient that always delivers — no domain verification needed. The region is inferred from your key's `bk_us1_` / `bk_eu1_` prefix, so there's no base URL to configure. The SDK auto-generates an `Idempotency-Key` per call, so retried sends are safe. ## 3. Try it Run the dev server and POST to the endpoint: ```bash npm run dev curl -X POST http://localhost:5173/api/send ``` The API responds with `202` — Bird has accepted the message and delivers it asynchronously: ```json { "id": "em_019c1930687b7bfa8a1b2c3d4e5f6789", "status": "accepted" } ``` ## Next steps - [TypeScript email SDK](/docs/sdks/typescript) — the full email surface: `send`, `get`, `list`, and the error model. - [Send your first email](/docs/get-started/send-your-first-email) — creating an API key and the full set of sandbox addresses. - [Sending domains](/docs/guides/email/sending-domains) — verify your own domain for production sending. - [Email API reference](/docs/api/reference/create-email-message) — the full request and response schema. --- title: "Send your first email" description: "Create a Bird API key and send your first email through the API in minutes — no domain verification required." source: https://bird.com/en-us/docs/get-started/send-your-first-email --- # Send your first email This is the fastest path from zero to a delivered email: create an API key, send through Bird's shared onboarding domain, and watch the result come back. No sending domain to verify, no DNS records to publish — that comes later, when you're ready for production. ## 1. Create an API key In the dashboard, go to [**Developers → API keys**](https://bird.com/dashboard/w/api-keys) and create a key. Keys are scoped to a region and look like `bk_us1_...` or `bk_eu1_...` — the region in the prefix tells you which API host to call: `https://us1.platform.bird.com` or `https://eu1.platform.bird.com`. ![The API Keys page in the Bird dashboard, listing keys with their masked prefix, scopes, and last-used time](/images/docs/dashboard-api-keys.png) The full key is shown **once**, at creation time. Copy it somewhere safe, then export it for the snippets below: ```bash export BIRD_API_KEY="bk_us1_..." ``` ## 2. Send an email Send from `onboarding@messagebird.dev` — Bird's shared onboarding domain, available to every workspace with no setup. Address it to `delivered@messagebird.dev`, a sandbox recipient that always delivers, so the result is deterministic without a real mailbox. ### curl ```bash curl -X POST https://us1.platform.bird.com/v1/email/messages \ -H "Authorization: Bearer $BIRD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "onboarding@messagebird.dev", "to": ["delivered@messagebird.dev"], "subject": "Hello from Bird", "html": "

My first Bird email.

" }' ``` If your key starts with `bk_eu1_`, call `https://eu1.platform.bird.com` instead. ### TypeScript ```bash npm install @messagebird/sdk ``` ```typescript import { BirdClient } from "@messagebird/sdk"; const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! }); const msg = await bird.email.send({ from: "onboarding@messagebird.dev", to: ["delivered@messagebird.dev"], subject: "Hello from Bird", html: "

My first Bird email.

", }); console.log(msg.id, msg.status); // "em_…", "accepted" ``` Using a different language or framework? The [per-SDK quickstarts](/docs/get-started/quickstarts/typescript/email) have the same three steps for every SDK. ## 3. See the result The API responds with `202` immediately — Bird has accepted the email and delivers it asynchronously: ```json { "id": "em_019c1930687b7bfa8a1b2c3d4e5f6789", "status": "accepted", "category": "transactional", "to": ["delivered@messagebird.dev"], "created_at": "2026-06-10T14:30:00Z" } ``` Fetch the message by its `em_` ID and watch the status move from `accepted` through `processed` to `delivered`: ```bash curl https://us1.platform.bird.com/v1/email/messages/em_019c1930687b7bfa8a1b2c3d4e5f6789 \ -H "Authorization: Bearer $BIRD_API_KEY" ``` ```typescript const result = await bird.email.get(msg.id); console.log(result.status); // "delivered" ``` Because you sent to `delivered@messagebird.dev`, the outcome is guaranteed: the message flows through Bird's real delivery pipeline — including the events and webhooks you'd see in production — but never touches a real mailbox. Want to see a bounce instead? Send to `bounce@messagebird.dev`. The full set of sandbox addresses (`delivered@`, `bounce@`, `softbounce@`, `complaint@`, `suppressed@`, `reject@`, `deferred@` — all `@messagebird.dev`) is covered in the [testing sandbox guide](/docs/guides/email/testing-sandbox). ## About the onboarding domain The shared `onboarding@messagebird.dev` sender is permanently available, but deliberately limited: - Apart from the sandbox addresses above, it only delivers to **verified members of your workspace** — any other recipient is rejected with a `422`. - Sends are capped at roughly **50 recipients per organization per day**; exceeding the cap returns a `429`. When you're ready to email real customers, [verify your own sending domain](/docs/guides/email/sending-domains) and put your own address in `from` — everything else in the request stays the same. A few request fields are not yet available in v1 -- `contact_id`, `topic_id`, and `in_reply_to_message_id` return a `422 unsupported_feature` if present. (To send at a future time, see [scheduled sending](/docs/guides/email/scheduled-sending).) ## Next steps - [Per-SDK quickstarts](/docs/get-started/quickstarts/typescript/email) — the same flow in your language and framework. - [Sending domains](/docs/guides/email/sending-domains) — verify your own domain for production sending. - [Testing sandbox](/docs/guides/email/testing-sandbox) — every sandbox address and the events it triggers. - [Email API reference](/docs/api/reference/create-email-message) — the full request and response schema. --- title: "Guides" description: "Task-oriented guides for building on Bird — sending email, deliverability, authentication, webhooks, billing, and more." source: https://bird.com/en-us/docs/guides --- # Guides Guides explain how Bird works and how to build on it: the platform concepts that apply across every channel, plus a deep dive into each channel you send on. Start with the **platform guides** if you're wiring up Bird itself — [Authentication](/docs/guides/authentication), how [Users, teams & roles](/docs/guides/users-teams-roles) and [Workspaces](/docs/guides/workspaces) fit together, and [Billing & usage](/docs/guides/billing-and-usage). The cross-cutting mechanics every integration leans on live here too: [Webhooks & events](/docs/guides/webhooks), [Idempotency](/docs/guides/idempotency), [Errors](/docs/guides/errors), and [Rate limits](/docs/guides/rate-limits). Then go channel-deep. The [Email guide](/docs/guides/email/overview) covers sending, sending domains and authentication, dedicated IPs, tracking and metrics, suppressions, and deliverability — everything from your first send to high volume. The [SMS guide](/docs/guides/sms/overview) covers sending text messages: recipients and senders, segments and categories, the message log, metrics, and events. New to Bird? The [quickstart](/docs/get-started/send-your-first-email) takes you from an API key to a delivered email; come back here for the why behind each piece. --- title: "Abuse & compliance" description: "How Bird keeps sending safe across every channel: the trust and safety pipeline, the reputation-based throttle and pause model, and what you encounter." source: https://bird.com/en-us/docs/guides/abuse-and-compliance --- # Abuse & compliance Bird is shared infrastructure: every sender's traffic travels over the same platform, and the reputation of that platform with mailbox providers and carriers is what gets everyone's messages delivered. Abuse protection exists to keep that shared reputation healthy — it is what makes your own deliverability possible. This page explains the channel-agnostic model; the concrete signals and thresholds for each channel live in the per-channel pages, starting with [email](/docs/guides/email/abuse-and-compliance). What you can and cannot send is governed by [Bird's Acceptable Use Policy](https://bird.com/legal/acceptable-use-policy); the [acceptable use summary](/docs/knowledge-base/policies/acceptable-use) is a plain-language walkthrough of it. This page covers how Bird detects and responds when sending goes wrong, whether through abuse or honest mistakes like a stale mailing list. ## The trust & safety pipeline Bird forwards trust-relevant control-plane actions — signup, login, MFA challenges, password resets, API-key creation and revocation, and billing events like top-ups and plan changes — to a trust & safety pipeline as an asynchronous event stream. Each event carries the action, its outcome, and request context such as IP address and user agent, and the pipeline assembles them into one risk profile per user. Two properties of this design matter to you as a developer: - **The stream is server-authoritative.** Risk signals are derived from what the backend observed, not from anything the client reports about itself. Browser-side signals (device fingerprinting on the dashboard) are best-effort enrichment — nothing depends on the client cooperating, so nothing breaks when it doesn't. - **Detection never blocks the happy path synchronously.** Events are dispatched asynchronously after the action completes; a slow or unavailable pipeline degrades signal collection, never your signup, login, or API call. There is no detection-induced latency on the request path. Today the pipeline collects signals. The next phase adds real-time scoring at sensitive decision points — authentication and billing — mapping risk to one of three outcomes: **allow** the action, **step up** with an additional challenge, or **block** it. The signal history being collected now is what makes those scores meaningful later. ## Throttling and pausing Sending behaviour feeds a per-workspace reputation on each channel. Healthy traffic — low bounce rates, low complaint rates, recipients who asked to hear from you — keeps reputation high and sending unrestricted. Bad behaviour degrades it, and Bird responds proportionally: - **Throttling** slows your sending rate while the problem signal persists, giving you time to correct course without a hard stop. - **Pausing** halts sending on the affected channel when the signal crosses the line from degraded to harmful — for example a bounce rate that indicates a purchased or badly stale list. The signals that drive this, and the indicative thresholds, are channel-specific: see [Abuse & compliance · Email](/docs/guides/email/abuse-and-compliance) for the email model. Throttling and pausing protect your sender reputation as much as Bird's — continuing to send traffic that mailbox providers are rejecting makes recovery harder, as explained in the [deliverability guide](/docs/guides/deliverability). ## Protections you encounter directly Most abuse protection is invisible when your traffic is healthy. A few controls are visible by design, because they sit on public surfaces that attackers probe: - **CAPTCHA on public auth endpoints.** Signup, login, forgot-password, and SMS-based MFA flows require a CAPTCHA challenge before the backend does any work. This is what keeps credential stuffing and signup automation from consuming the platform. - **SMS-OTP country allowlists.** One-time passcodes over SMS are only sent to phone numbers in a supported set of countries; enrollment with a number outside the allowlist is rejected with `422 phone_country_not_allowed`. This closes off SMS pumping — fraud schemes that trigger OTP sends to premium-rate numbers. - **Per-phone and per-IP limits.** OTP sends and authentication attempts are rate-limited per phone number and per source IP, with progressive delays on repeated failures. These controls protect your account as well as the platform — an attacker who can burn your SMS budget or brute-force your login is your problem before it is Bird's. ## Next steps - [Abuse & compliance · Email](/docs/guides/email/abuse-and-compliance) — the email-specific signals, thresholds, and automatic suppression behaviour - [Deliverability](/docs/guides/deliverability) — how reputation translates into inbox placement - [Acceptable use summary](/docs/knowledge-base/policies/acceptable-use) — plain-language guide to what content and sending practices are permitted --- title: "Authentication & API keys" description: "How Bird API requests authenticate with bearer keys — key anatomy, scopes, revocation, and the delegated OAuth path for the CLI and MCP server." source: https://bird.com/en-us/docs/guides/authentication --- # Authentication & API keys Every programmatic request to the Bird API authenticates with an API key passed as a bearer token. Keys belong to a workspace, carry a fixed set of permissions chosen at creation, and are shown in full exactly once. ## How requests authenticate Pass your key in the `Authorization` header on every request: ```bash curl -X POST https://us1.platform.bird.com/v1/email/messages \ -H "Authorization: Bearer bk_us1_Ab3xKq9mP2wR5tY8uI1oL4nJ..." \ -H "Content-Type: application/json" \ -d '{ "from": "hello@yourdomain.com", "to": ["delivered@messagebird.dev"], "subject": "Hi", "text": "Hello." }' ``` The region in the key prefix tells you which host to call: `bk_us1_...` keys go to `https://us1.platform.bird.com`, `bk_eu1_...` keys to `https://eu1.platform.bird.com`. Sending a key to the wrong regional host returns `421 misdirected_request` — see [Regions](/docs/api/regions). A missing or invalid key returns `401`. A valid key that lacks the permission an endpoint requires returns `403`. Full request/response semantics live in the [authentication reference](/docs/api/authentication). ## Anatomy of a key ```text bk_us1_Ab3xKq9mP2wR5tY8uI1oL4nJ... └┬┘└┬┘ └──────────┬──────────┘└┬┘ │ │ payload checksum │ └ region (routes the request) └ Bird key prefix ``` - **Prefix** — `bk_{region}_` identifies the credential type and region. The fixed prefix means GitHub secret scanning can detect a leaked Bird key in a public repository, and the region segment routes your request to the right host. - **Payload** — a long random string with 128+ bits of entropy. - **Checksum** — the last 6 characters are a checksum of the rest of the key, so an SDK or the API can reject a mistyped or truncated key immediately, before it is ever looked up. The full key is returned **once**, in the response that creates it. Bird stores only a cryptographic hash (HMAC-SHA-256) — there is no way to retrieve the plaintext again, for you or for Bird. What you can see afterwards is the `key_prefix` (the first 12 characters, e.g. `bk_us1_Ab3x`) and a stable 12-character `fingerprint` for matching a key in logs and audit trails without exposing its value. If you lose a key, you don't recover it — you [revoke it](#revoking-a-key) and create a new one. ## Creating a key Create keys in the dashboard under [**Developers → API keys**](https://bird.com/dashboard/w/api-keys), or programmatically via [`POST /v1/api-keys`](/docs/api). Creating and revoking keys requires the `api_keys:write` permission, which the Admin and Developer workspace roles hold — see [Users, teams & roles](/docs/guides/users-teams-roles). ![The API Keys page in the Bird dashboard, listing keys with their masked prefix, scopes, and last-used time](/images/docs/dashboard-api-keys.png) A key is created with a name and one or more scopes: ```bash curl -X POST https://us1.platform.bird.com/v1/api-keys \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "Email operations production key", "scopes": [{ "scope": "emails", "level": "write" }] }' ``` The `201` response includes the `token` field — the only time the full key is ever returned. Store it in your secret manager immediately. The scope set is **immutable**: there is no endpoint to add or remove scopes on an existing key. To change what a key can do, create a new key with the right scopes and revoke the old one. This keeps every key's permissions exactly what they were when it was issued and audited. You can list keys with `GET /v1/api-keys` (revoked keys are excluded unless you pass `include_revoked=true`) and fetch one with `GET /v1/api-keys/{api_key_id}`. Each key reports `last_used_on` (day precision) so you can spot stale keys. ## Scopes & levels Each scope on a key is a `{scope, level}` pair, where `level` is `read` or `write` (`write` includes `read`). API keys can hold data-plane scopes: | Scope | `read` | `write` | | ------------------ | ----------------------------------------- | ------------------------------------------- | | `emails` | Read sent messages and delivery status | Send email | | `email_management` | Read suppressions and email configuration | Manage suppressions and email configuration | Control-plane operations — workspace members, settings, key issuance, IP pools — are deliberately not grantable to API keys; they remain dashboard-only. More scopes will be added as products gain programmatic surfaces. Grant the narrowest set that works: a key that only sends email should hold `emails:write` and nothing else. ## Revoking a key ```bash curl -X POST https://us1.platform.bird.com/v1/api-keys/{api_key_id}/revoke \ -H "Authorization: Bearer " ``` Revocation is permanent — there is no un-revoke. The key record is preserved (with `revoked_at` set) for audit, and revoking an already-revoked key returns `409`. Revocation takes effect almost immediately: the revoked key is invalidated in Bird's validation cache as soon as you revoke it, and in the worst case a request may still succeed for up to a few minutes (the cache window is 1–5 minutes) before every request with that key returns `401`. There is no rotate endpoint. To rotate a key with zero downtime, overlap two keys: 1. Create a new key with the same scopes. 2. Deploy the new key to your services. 3. Watch the old key's `last_used_on` until traffic has moved. 4. Revoke the old key. ## Keys belong to the workspace An API key is bound to exactly one workspace — it authenticates as the workspace, not as the person who created it. That has two practical consequences: - **Keys survive departures.** When an employee leaves and their user account is removed, the keys they created keep working. You never have a production outage because the person who clicked "create" left the company. (Their departure is still a good prompt to rotate keys they had access to.) - **One workspace per key.** A key cannot reach resources in another workspace, and it can never perform organization-level operations (billing, org members, org settings). If you operate multiple workspaces, create a key in each. ## The delegated path: OAuth tokens for the CLI and MCP server API keys are for services. When a _person_ uses the [Bird CLI](/docs/ai/cli-for-agents) or the [Bird MCP server](/docs/ai/mcp-server), those tools authenticate via OAuth instead: you log in as yourself in the browser, choose a workspace and a subset of your own permissions, and the tool receives a short-lived `bt_{region}_...` user token. These tokens are capped to what you personally hold, revocable per tool, and are never something you copy-paste or store in a secret manager — don't use them in place of API keys for server workloads. ## Next steps - [API keys reference](/docs/api) — full endpoint and schema documentation - [Authentication reference](/docs/api/authentication) — header semantics and error responses - [Regions](/docs/api/regions) — regional hosts and routing - [Users, teams & roles](/docs/guides/users-teams-roles) — who can manage keys --- title: "Billing & usage" description: "How Bird billing works for developers: plans and tiers, usage metering against your included volume, the shared wallet, invoices, and operational limits." source: https://bird.com/en-us/docs/guides/billing-and-usage --- # Billing & usage Billing is organization-scoped: one plan, one wallet, and one invoice stream are shared across all of an organization's workspaces. This page covers the parts of billing a developer actually touches: how usage is metered, how to read it, and how your plan tier shapes the operational limits your integration runs against. For account operations — pricing details, payment methods, tax — see the Knowledge base ([Plans & pricing](/docs/knowledge-base/billing/plans-and-pricing), [Usage & invoices](/docs/knowledge-base/billing/usage-and-invoices)). All billing endpoints live under `/v1/organization/billing/...`, are gated by the `org:billing` scope, and are session-authenticated — billing is managed by people (owners and billing admins, see [Users, teams & roles](/docs/guides/users-teams-roles)), not by API keys. ## Plans and tiers Your organization subscribes to one plan. The plan catalog is available at `GET /v1/billing/plans`; your current subscription lives at `GET /v1/organization/billing/plan`, and plan changes go through `POST /v1/organization/billing/plan/change`. Each plan carries a **tier** — free, startup, growth, or custom — and the tier is what your integration feels operationally: - **Included sending volume.** Each plan includes a monthly allotment of email sends; sends beyond it are metered as overage. - **Rate-limit ceilings scale with tier.** Rate limits resolve as the maximum of a base value, your tier's value, and any per-organization override. The `email_send` group is the clearest example: 10 requests/min at base (free), 100/min on startup, 1,000/min on growth, with custom-tier ceilings set by arrangement. Higher tiers raise the list/read ceilings too. The model and headers are described in [Rate limits](/docs/guides/rate-limits); per-channel numbers live on the channel pages. - **Resource limits.** Caps such as how many workspaces, sending domains, IP pools, and dedicated IPs your organization can hold are set per plan and adjustable per organization — if you hit one, contact support rather than working around it. If you are approaching a ceiling, that is a conversation, not a wall: per-organization overrides above the tier values are a normal part of scaling on Bird. ## Usage metering Every send is metered against your organization's current billing period. `GET /v1/organization/billing/usage` returns the live picture — per product, for the current period: ```bash curl https://us1.platform.bird.com/v1/organization/billing/usage \ -H "Authorization: Bearer " \ -H "X-Organization-Id: " ``` ```json { "period": { "start": "2026-06-01T00:00:00Z", "end": "2026-06-30T23:59:59Z" }, "items": [ { "product": { "slug": "email_transactional", "name": "Email sends" }, "pricing_model": "metered", "metered": { "quantity": 1284300, "included": 2000000, "overage": 0, "utilization_pct": 64.2 } } ] } ``` Each item is one of two pricing models, named by the `pricing_model` discriminator: - **`metered`** — products with an included allotment on your plan (email sends). The `metered` object gives `quantity` used, `included` on the plan, `overage` beyond it, and `utilization_pct`. One accepted message is one metered unit. - **`rate_card`** — pay-as-you-go products billed per use against a rate card. The `transactional` object gives a `transaction_count` and the `net_amount` spent so far this period. By default the response covers every product you are subscribed to plus any pay-as-you-go product you have used this period; pass `products[]` to filter. The same data backs the [**Billing and payments → Usage**](https://bird.com/dashboard/o/billing/usage) page in the dashboard. ## The wallet Each organization has a wallet — a shared prepaid balance that all workspaces draw from. Top-ups and plan charges both flow through it: `POST /v1/organization/billing/wallets/{wallet_id}/topup` initiates a top-up payment, and `GET /v1/organization/billing/wallets/{wallet_id}/transactions` is the full ledger of credits and charges, so you can reconcile every movement programmatically. To avoid surprises, set billing notification addresses: the `notification_emails.billing` list (managed via `PATCH /v1/organization`, or in the dashboard under [**Billing and payments**](https://bird.com/dashboard/o/billing/settings)) receives low-balance warnings, invoice notifications, and plan-change confirmations. Point it at a team alias, not an individual — billing alerts that land in one person's inbox are billing alerts that get missed. ## Invoices Invoices are issued per organization and available at `GET /v1/organization/billing/invoices`, with individual invoices (including line items) at `GET /v1/organization/billing/invoices/{invoice_id}`. Invoices reflect the same metering shown by the usage endpoint: your plan's recurring charge plus any metered overage and pay-as-you-go spend for the period. For how invoices are structured, payment timing, and dunning, see [Usage & invoices](/docs/knowledge-base/billing/usage-and-invoices) in the Knowledge base. ## Next steps - [Rate limits](/docs/guides/rate-limits) — the base/tier/override model your tier feeds into - [Plans & pricing](/docs/knowledge-base/billing/plans-and-pricing) — what each plan includes - [Usage & invoices](/docs/knowledge-base/billing/usage-and-invoices) — reading invoices, payment methods, and billing operations - [Users, teams & roles](/docs/guides/users-teams-roles) — who can manage billing (`org:billing`) --- title: "Deliverability" description: "Why sender reputation decides whether your messages reach recipients, what shapes it across every channel, and which inputs you control on Bird." source: https://bird.com/en-us/docs/guides/deliverability --- # Deliverability Sending a message and having it arrive are two different problems. Every channel Bird supports — email today, others as they come online — sits behind gatekeepers (mailbox providers, carriers) that decide whether your message reaches the recipient, lands in a junk folder, or disappears. That decision is driven by one asset: your **sender reputation**. This page explains the model that holds across channels; the per-channel mechanics live in the leaf pages, starting with [email](/docs/guides/email/deliverability). ## Reputation is the asset Gatekeepers do not evaluate messages in isolation — they evaluate senders. Every message you send either builds or erodes a track record attached to your sending identity, and that track record is what gets consulted the next time you send. Reputation is earned slowly and lost quickly: months of consistent, wanted traffic build trust that a single bad campaign can burn. Treat it like the production asset it is. Four behaviors dominate how gatekeepers score you: - **Authentication.** Proving the message really comes from the identity it claims. Unauthenticated traffic is indistinguishable from spoofing, and gatekeepers increasingly refuse it outright rather than merely scoring it down. - **Recipient engagement.** Gatekeepers watch what recipients do — open, read, reply, delete-without-reading, mark as spam. Sending to people who want your messages is the single strongest positive signal, and no technical configuration substitutes for it. - **Complaint and bounce behavior.** Repeatedly messaging addresses that do not exist, or recipients who report you, is the classic spammer signature. Reacting to bounces and complaints — and never messaging those recipients again — is table stakes. - **Sending consistency.** Reputation systems trust patterns. Steady, predictable volume from a known identity scores well; sudden spikes from a cold or dormant identity look exactly like compromised infrastructure, regardless of how legitimate the traffic is. ## The inputs you control Bird sends your traffic through its dedicated sending infrastructure, and the platform-level reputation work — keeping shared infrastructure healthy, pacing traffic, processing delivery feedback — happens for you. Two levers remain yours, and they are the same two on every channel: - **Prove your identity.** Verifying your sending identity (for email, domain authentication via DNS records) is the gate before traffic flows, and it is what lets your reputation accrue to _you_ rather than to a generic platform identity. Gatekeepers can only trust a sender they can recognize — the [DMARC policy generator](/tools/dmarc-policy) builds or validates your DMARC record, and the [Email header analyzer](/tools/email-analyzer) confirms a real message passed SPF, DKIM, and DMARC. - **Isolate your reputation — or pool it.** By default you send through shared infrastructure, where Bird's established reputation carries you and your track record blends with other customers'. Dedicated infrastructure (for email, dedicated IPs and pools) gives you an identity that is judged on your traffic alone — better when your volume is high and clean, worse when it is low or erratic, because a dedicated identity starts cold and must earn trust through warmup. Everything else — list hygiene, opt-in practice, content quality, send cadence — is behavior rather than configuration, and it matters more than any of the configuration. ## Next steps - [Email deliverability](/docs/guides/email/deliverability) — the email-specific mechanics: authentication records, dedicated IPs and warmup, and reputation monitoring - [Knowledge base: warming up your sending](/docs/knowledge-base/deliverability/warming) — non-engineering best practice for ramping volume and keeping lists clean - [Abuse & compliance](/docs/guides/abuse-and-compliance) — the platform rules that protect everyone's deliverability, including yours --- title: "Abuse & compliance · Email" description: "The email signals behind throttling and pausing: bounce and complaint rates, unsubscribe behaviour, automatic suppression, and the levers you control." source: https://bird.com/en-us/docs/guides/email/abuse-and-compliance --- # Abuse & compliance · Email Email sending runs on the shared abuse model described in the [Abuse & compliance overview](/docs/guides/abuse-and-compliance): healthy traffic sends unrestricted, degraded reputation throttles your sending rate, and harmful signals pause it. This page covers the email-specific part — which signals drive that reputation, what Bird does automatically in response, and which levers you control. If your sending was throttled or paused and you want the non-developer walkthrough, see [Why was my sending throttled or paused?](/docs/knowledge-base/troubleshooting/throttled-paused). ## The signals that drive reputation Bird derives your email reputation from the same per-recipient events you can already observe through [webhooks and the events API](/docs/guides/email/events): - **Bounce rate** — the share of sends ending in `email.bounced` with `bounce_type: "hard"`. Hard bounces mean the address does not exist; a high rate is the signature of a purchased, scraped, or badly stale list, and it is the signal mailbox providers punish hardest. - **Complaint rate** — the share of delivered mail reported as spam, surfaced as `email.complained` through mailbox-provider feedback loops. Complaints measure whether recipients wanted your mail at all. - **Unsubscribe behaviour** — `email.unsubscribed` (your footer link) and `email.list_unsubscribed` (the provider-rendered one-click unsubscribe). Unsubscribes are a normal part of list life, but a spike on a campaign is an early warning that your targeting or consent has drifted — recipients who can't find the unsubscribe link report spam instead. Because these are the same events your webhook endpoint receives, you can monitor exactly what Bird monitors. Watching your own bounce and complaint rates per campaign is the single best way to never meet the throttling machinery at all. ## Indicative thresholds The thresholds below are guidance, not contract — enforcement weighs volume, history, and trend, so a small test batch with two bounces is treated differently from a million-recipient campaign with the same rate: - A hard-bounce rate approaching **~5%** is the ballpark where sending gets paused. Healthy lists run far below 1%; mailbox providers start degrading your inbox placement well before 5%. - Complaint rates need to stay **well under 0.3%** — Gmail and Yahoo enforce 0.3% as a hard ceiling for bulk senders, and sustained rates above ~0.1% already hurt placement. Bird throttles before your complaint rate damages the shared sending infrastructure. If you expect an unusual send — a re-engagement campaign to an old segment, a list migration from another provider — clean the list first rather than letting the bounce rate discover the problem for you. ## What Bird does automatically: suppression Beyond throttling, the delivery pipeline acts on individual recipients immediately — you never have to process a bounce or complaint yourself. The signals above each add the address to your workspace [suppression list](/docs/guides/email/suppressions), and every automatic addition fires an `email_suppression.created` webhook event so your systems can mirror the change: | Signal | Suppression scope | | ------------------------------------------------------------- | ---------------------------------------------------------- | | Hard bounce (`email.bounced`, `email.out_of_band_bounce`) | All mail — the address does not exist | | Spam complaint (`email.complained`) | Non-transactional mail only — transactional still delivers | | Unsubscribe (`email.unsubscribed`, `email.list_unsubscribed`) | Non-transactional mail only — transactional still delivers | Future sends to a suppressed address are rejected up front with `email.rejected` and `rejection_reason: "recipient_suppressed"` — they never reach a mail server, so they never count against your reputation again. Suppression is the mechanism that makes one bad signal per address sufficient: the bounce that hurt you once cannot keep hurting you. Soft bounces and deferrals (`email.deferred`) do not suppress and do not carry the same reputation weight — they are transient failures Bird retries automatically. ## Your levers: list hygiene and consent Throttling responds to signals; the signals respond to how you build and maintain your list. Two practices prevent nearly every email pause: - **List hygiene** — never import purchased or scraped lists, remove addresses that have not engaged in months, and validate addresses at collection time (a typo'd signup is a future hard bounce). The practical workflow is in the [list hygiene guide](/docs/knowledge-base/deliverability/list-hygiene). - **Consent** — send only to recipients who asked, make unsubscribing effortless, and keep marketing in the `marketing` [category](/docs/guides/email/categories) so suppression policy and unsubscribe handling apply correctly. Most complaint spikes trace back to mail the recipient never opted into. If sending has already been paused, fix the source list before requesting reinstatement — resuming with the same list reproduces the same signals. The [throttled-or-paused walkthrough](/docs/knowledge-base/troubleshooting/throttled-paused) covers the recovery path step by step. ## Next steps - [Abuse & compliance overview](/docs/guides/abuse-and-compliance) — the channel-agnostic trust & safety pipeline and throttle/pause model - [Suppressions](/docs/guides/email/suppressions) — the four suppression reasons, the `applies_to` policy, and the management API - [Email events reference](/docs/guides/email/events) — full payloads for `email.bounced`, `email.complained`, the unsubscribe events, and bounce classification - [Why was my sending throttled or paused?](/docs/knowledge-base/troubleshooting/throttled-paused) — the non-developer walkthrough - [List hygiene](/docs/knowledge-base/deliverability/list-hygiene) — keeping bounce and complaint rates low in practice --- title: "Attachments" description: "Attach files and inline images to an email send — base64 content, the field contract, size budget, blocked types, and downloading attachments back." source: https://bird.com/en-us/docs/guides/email/attachments --- # Attachments Attach files to a send by adding an `attachments` array to the `POST /v1/email/messages` payload. Each entry carries the file's bytes base64-encoded in `content`, plus a `filename`. The same array works on a [batch](/docs/guides/email/sending-bulk) item. Full request and response schemas are in the [API reference](/docs/api/reference/create-email-message). ## A send with one attachment ```bash curl -X POST https://us1.platform.bird.com/v1/email/messages \ -H "Authorization: Bearer bk_us1_..." \ -H "Content-Type: application/json" \ -d '{ "from": "hello@yourdomain.com", "to": ["delivered@messagebird.dev"], "subject": "Your invoice", "html": "

Thanks for your order — your invoice is attached.

", "attachments": [ { "filename": "invoice.pdf", "content": "JVBERi0xLjcKJ...", "content_type": "application/pdf" } ] }' ``` `content` is the raw file bytes, base64-encoded. `content_type` is optional — when you omit it, Bird infers the MIME type from the `filename` extension. Everything else about the send works exactly as in [sending email](/docs/guides/email/sending-email): the `202`, the async model, tags, and metadata are unchanged by the presence of attachments. ## The attachment fields | Field | Type | Required | Notes | | :------------- | :----- | :------- | :----------------------------------------------------------------------------------------------- | | `filename` | string | yes | 1–255 characters; shown to the recipient. No line breaks or control characters. | | `content` | string | yes | Base64-encoded file bytes. | | `content_type` | string | no | MIME type; inferred from the filename extension when omitted. | | `content_id` | string | no | 1–128 chars, `[A-Za-z0-9._-]`. Set it to render the file inline (see below) instead of attached. | An email carries up to 20 attachments (`attachments` is capped at 20 items). The `path` field — give Bird a URL and it fetches the file for you — is a preview feature and currently unavailable; supply `content` instead. ## Inline images To embed an image in the HTML body rather than attach it, give the attachment a `content_id` and reference it from the markup with a `cid:` URL: ```json { "html": "

Welcome aboard!

", "attachments": [ { "filename": "banner.png", "content": "iVBORw0KGgoAAAANS...", "content_type": "image/png", "content_id": "welcome-banner" } ] } ``` The `content_id` is the join between the `cid:` reference and the attachment. Each inline image needs a unique `content_id` within the send — a duplicate is rejected with a `422`. An attachment with no `content_id` is delivered as a regular file attachment. ## Size budget Bird rejects a send whose **estimated generated message size** exceeds **20 MB** with a `413`. The estimate is the HTML body plus the text body plus every attachment measured _after_ base64 encoding — encoding inflates raw bytes by roughly 4/3, so a 15 MB file costs about 20 MB against the budget. As a rule of thumb, keep total raw attachment content at or below **15 MB** to leave headroom for encoding and MIME wrapping. That cap is what Bird accepts; it is not what every mailbox accepts. Downstream limits vary by provider and by tenant policy — Gmail and Outlook.com document 25 MB, Exchange Online defaults to 35 MB but admins can lower it, and on-prem Exchange often defaults to 10 MB. A message near Bird's 20 MB cap can be accepted by Bird and still bounce at the recipient's server, so size attachments for the inboxes you actually send to. ## Blocked file types Executable and script attachments are rejected at validation time with a `422`, matched on either the `content_type` or the `filename` extension. The list is deliberately narrow — common mail-borne executable and script formats, not a virus scanner — and covers types like `.exe`, `.dll`, `.msi`, `.bat`, `.cmd`, `.scr`, `.jar`, `.js`, `.vbs`, `.ps1`, `.sh`, `.hta`, and `.lnk`, along with their MIME equivalents (`application/x-msdownload`, `application/java-archive`, `text/javascript`, and similar). Zip the file or host it behind a link if you need to send one of these. ## In a batch Each item in a [batch send](/docs/guides/email/sending-bulk) can carry its own `attachments`, with the same field contract and the same per-message 20 MB cap. One extra limit applies at the batch level: the serialized JSON request body for the whole batch has a hard 20 MB cap. Because attachments are base64-encoded inside that body, a batch of attachment-heavy messages reaches the body cap quickly — split large attachment sends across several batches, or send them individually. ## Reading and downloading attachments API reads never echo attachment bytes. `GET /v1/email/messages/{id}` returns an `attachments` array of metadata only — each entry has the attachment `id`, `filename`, `content_type`, `size` (decoded bytes), and `inline`: ```json { "attachments": [ { "id": "ea_019c...", "filename": "invoice.pdf", "content_type": "application/pdf", "size": 215432, "inline": false } ] } ``` To get the raw bytes back, call `GET /v1/email/messages/{message_id}/attachments/{attachment_id}` ([reference](/docs/api/reference/get-email-message-attachment)). It streams the file with its original content type and a `Content-Disposition` filename. Two conditions gate it: - **Content storage must be on** for the workspace (it is on by default; see the [async model](/docs/guides/email/sending-email#the-async-model-what-202-means)). With storage off, there is nothing to download. - **Attachments are retained for 30 days** after the send. After that the download returns `410 Gone`. A `404` means the message has no stored content or no attachment with that ID; a `425 Too Early` means the attachment is still being stored and the request can be retried in a moment. ## Next steps - [Sending email](/docs/guides/email/sending-email) — the rest of the send payload: recipients, content, tags, and the async model - [Bulk sending](/docs/guides/email/sending-bulk) — batches and where attachments fit across many messages - [API reference: create a message](/docs/api/reference/create-email-message) — the full request schema, including `attachments` - [API reference: download an attachment](/docs/api/reference/get-email-message-attachment) — the retrieval endpoint and its status codes --- title: "Audiences" description: "Group contacts into reusable audiences, add or remove members, and reuse a saved list instead of assembling the same addresses every time." source: https://bird.com/en-us/docs/guides/email/audiences --- # Audiences An audience is a named list of [contacts](/docs/guides/email/contacts). You build an audience once and reuse it, so reaching the same group again means naming the list rather than assembling the addresses every time. A contact can belong to many audiences, and you add or remove members as people join and leave. You manage audiences in the Email app, or with the `bird` [CLI](/docs/cli). ## The Audiences page The [**Audiences**](https://bird.com/dashboard/w/email/audiences) page (**Email → Audiences**) lists your audiences by name, type, and when they were created. Create one with a name and an optional description, then open it to see and manage its members. As with contacts, viewing needs the `email_marketing` read permission and managing needs write. ![The Audiences page in the Bird dashboard, listing audiences by name and type with a New audience button](/images/docs/dashboard-email-audiences.png) ## Members Opening an audience shows its members, most recently added first, with each contact's email, name, and the date it joined. ![The detail page of the Newsletter subscribers audience in the Bird dashboard, showing its members table with each member's email, name, and join date](/images/docs/dashboard-email-audience-detail.png) There are two ways to add people: - **Add existing or new contacts** by pasting email addresses on the audience, one per line, up to 1000 at a time. Bird matches each address to an existing contact or creates one, then assigns them all to the audience in a single step. - **Assign at import time** by dropping contacts straight into an audience as you [import or upsert them](/docs/guides/email/contacts#importing-and-syncing-contacts). Removing a member takes the contact out of that audience only. The contact itself stays in your workspace and in any other audiences it belongs to. From the CLI the same operations are `bird audiences add-contacts ` and `bird audiences remove-contacts `. ## Sending to an audience Sending one message to a whole audience is a broadcast, rather than listing the addresses yourself. Broadcasts are not yet available; to reach many recipients today, send a [batch](/docs/guides/email/sending-bulk) or fan out over single sends. ## Next steps - [Contacts](/docs/guides/email/contacts) — the recipient records and typed properties an audience is built from - [Bulk sending](/docs/guides/email/sending-bulk) — reaching many recipients today with batches - [Suppressions](/docs/guides/email/suppressions) — the workspace list of addresses Bird will not deliver to --- title: "BIMI" description: "Publish a BIMI record on your Bird-verified sending domain so your brand logo appears next to authenticated mail in Gmail, Yahoo, and other supporting inboxes." source: https://bird.com/en-us/docs/guides/email/bimi --- # BIMI BIMI (Brand Indicators for Message Identification) displays your verified brand logo next to your messages in supporting inboxes — Gmail, Yahoo Mail, Apple Mail, and others. It is a visual reward for getting email authentication right: a recipient scanning their inbox sees your logo instead of generic initials, which builds trust, reinforces your brand on every send, and makes spoofed mail easier to spot because the imitation arrives without it. BIMI is a DNS standard, not a Bird feature — there is no BIMI API in Bird's v1 surface. You publish the record yourself at your DNS provider, on a domain you have already verified with Bird. This page covers the prerequisites, the record, the logo format, and the certificate some providers require. ## Prerequisites BIMI builds on top of email authentication, so everything in [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc) comes first: - **A verified sending domain on Bird.** Your domain's DKIM, return-path, and DMARC records must be published and verified — see [Sending domains](/docs/guides/email/sending-domains) if you have not set one up yet. - **DMARC at enforcement.** This is the key gotcha: BIMI requires a DMARC policy of `p=quarantine` or `p=reject`, which is **stricter than what Bird's send gate accepts**. Bird only requires that a valid DMARC record exists — its recommended starting value uses `p=none` — so a domain can be fully verified and sending on Bird while still not qualifying for BIMI. Mailbox providers also evaluate the policy at your organizational domain (the registrable apex, e.g. `example.com`), so a `p=quarantine` on a subdomain does not count if the apex policy is `p=none`, and an apex record with `sp=none` weakening subdomain enforcement disqualifies you too. ### Moving DMARC to enforcement Do not jump from `p=none` to `p=reject` in one step. At enforcement, mail that fails DKIM and SPF alignment gets quarantined or rejected — and that includes any legitimate mail flowing through senders you forgot about (a CRM, a ticketing system, an old newsletter tool). The safe path: 1. Stay on `p=none` and read your aggregate reports (`rua`) for a few weeks to inventory every source sending as your domain. Mail sent through your verified Bird domain passes DKIM with alignment, so it is safe at any policy. 2. Fix or retire unauthenticated sources, then move to `p=quarantine`, optionally ramping with `pct=` (e.g. `pct=25`) to enforce on a fraction of failing mail first. 3. Once the reports are clean at full quarantine, move to `p=reject` if you want the strongest policy. Both `quarantine` and `reject` satisfy BIMI. The [DMARC policy generator](/tools/dmarc-policy) can help you write the record for each stage. ## The BIMI DNS record BIMI is a single TXT record at the `default._bimi` selector under your domain: | Type | Host | Value | | ---- | --------------------------- | ------------------------------------------------------------------------------------ | | TXT | `default._bimi.example.com` | `v=BIMI1; l=https://example.com/brand/logo.svg; a=https://example.com/brand/vmc.pem` | - `v=BIMI1` — the version tag, always first. - `l=` — an HTTPS URL to your logo in SVG Tiny PS format (see below). - `a=` — an HTTPS URL to your Verified Mark Certificate (see below). Omit the tag or leave it empty (`a=;`) if you are publishing without a certificate; providers that require one simply will not show the logo. Publish it like any other TXT record at your DNS provider — the same place you published your Bird verification records. Like DMARC, the record is looked up at your sending domain first, falling back to the organizational domain, so a single record at the apex covers subdomains that do not declare their own. The [BIMI record generator](/tools/bimi) can build and validate this record before you publish it. ## Logo requirements The `l=` URL must point to an SVG in the **SVG Tiny Portable/Secure (SVG Tiny PS)** profile — a locked-down SVG subset that forbids scripts, external references, and animation. A regular SVG export from a design tool will usually be rejected until converted; free SVG Tiny PS converters and validators are available from the BIMI Group and certificate authorities. Beyond the profile: - **Square aspect ratio**, with the logo centered — clients render it in a circle or rounded square, so keep meaningful content away from the corners. - **Solid background color**, not transparent — transparency renders unpredictably across clients. - **Served over HTTPS** from a publicly reachable URL, ideally small (under 32 KB is a common guideline). - The SVG's `` element should contain your brand name. ## Verified Mark Certificates (VMC) A VMC is a certificate from an authorized certification authority (currently DigiCert and Entrust) that attests you own the logo — typically by tying it to a registered trademark, though some CAs also offer marks based on prior use. The CA validates your organization and the mark, then issues a `.pem` certificate that embeds the logo itself; you host it at the `a=` URL. Whether you need one depends on the mailbox provider: - **Gmail requires a VMC** (or its sibling, the Common Mark Certificate). Without one, Gmail will not display your logo even if your record and DMARC policy are perfect. - **Yahoo Mail does not require a certificate**, but it does require DMARC enforcement at the organizational domain and applies its own sender-reputation bar — new or low-volume senders may not get the logo immediately even with a valid record. - Other BIMI-aware clients fall somewhere in between; requiring a VMC is the trend. If your audience is mostly Gmail, budget for the certificate — it is the larger cost and lead time in a BIMI rollout, since trademark validation takes time. You can publish the record with only `l=` first and add `a=` when the certificate is issued. ## Verifying it works Bird does not check or report BIMI status — verify it with the standard ecosystem tools: - Run your domain through a **BIMI inspector** (the BIMI Group and several email-tooling vendors offer free ones). These check the record syntax, fetch and validate the SVG, verify the VMC, and confirm your DMARC policy qualifies — and tell you exactly which requirement is unmet. - Send a real message through your Bird domain to a Gmail and a Yahoo mailbox and look for the logo. Allow for propagation and provider-side evaluation: the logo often appears within hours of a correct setup but can take longer, especially on Yahoo where reputation factors in. If the logo does not appear, the cause is almost always one of: DMARC not at enforcement on the organizational domain, an SVG that fails the Tiny PS profile, a missing VMC on Gmail, or insufficient sending reputation. ## Next steps - Set up or check your domain's verification records: [Sending domains](/docs/guides/email/sending-domains) - Understand the authentication records BIMI builds on, including the DMARC policy upgrade: [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc) --- title: "Bounce domain" description: "The return-path (bounce) hostname on your sending domain — what it does, how to customize it, and why it's what gives you SPF alignment without an apex record." source: https://bird.com/en-us/docs/guides/email/bounce-domain --- # Bounce domain Your **bounce domain** is the return-path hostname on a [sending domain](/docs/guides/email/sending-domains) — the envelope-from address that bounces and delivery feedback are sent to. Pointing it at Bird with one CNAME does two jobs at once: it lets Bird collect and process bounces for you, and it gives you SPF alignment without publishing anything at your domain apex. It's one of the three records that gate sending, so every domain has one. This page covers the return-path specifically. For the full record set and the verification lifecycle, see [sending domains](/docs/guides/email/sending-domains); for what each record proves, see [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc). ## The record The return-path is a CNAME under your sending domain that resolves to Bird's bounce infrastructure in your region: | Type | Host | Value | | ----- | ------------------ | -------------------------- | | CNAME | `send.example.com` | `<region>.bounce.bird.com` | The value is region-specific — `us1.bounce.bird.com` or `eu1.bounce.bird.com` — so copy it from your domain's detail page (open it from the [**Domains**](https://bird.com/dashboard/w/email/domains) page) or the domain resource rather than constructing it. The host defaults to `send.` under your sending domain. ## Customizing the hostname You choose the label when you register the domain — pass it as `return_path.name` and Bird composes the full hostname under your sending domain: ```bash curl -s https://us1.platform.bird.com/v1/email/domains \ -H "Authorization: Bearer $BIRD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "domain": "mail.acme.com", "return_path": { "name": "bounce" } }' ``` That registers `bounce.mail.acme.com` as the return-path instead of the default `send.mail.acme.com`. Omit `return_path` and it defaults to `send`. The hostname is a **per-workspace** choice: two workspaces sharing a domain can each pick their own return-path (or share one), and one workspace's choice never affects another's. ## Why it covers SPF Receivers evaluate SPF against the envelope-from domain, not the visible `From:` address — and your envelope-from is the return-path hostname. Because the verified CNAME resolves to Bird's bounce infrastructure, that infrastructure's SPF authorization applies, and it _aligns_ with your domain because the return-path is a subdomain of it. So SPF passes and aligns with one CNAME, and you don't publish (or maintain) an `include:` at your apex. The full reasoning is in [Where's SPF?](/docs/guides/email/dkim-spf-dmarc#wheres-spf). ## It gates sending — and can't be removed The return-path CNAME is part of the send gate: `capabilities.sending` only verifies once DKIM, the return-path CNAME, and a DMARC policy are all in place. Until the return-path verifies, the domain can't send. For the same reason, the return-path can't be removed — every send needs somewhere for bounces to go. You can _change_ the hostname, but you can't unset it: a `PATCH` that tries to null it out is rejected. Changes are staged safely — a hostname that's already verified is never swapped for an unverified one. Bird verifies the new return-path alongside your active one and promotes it only once the new CNAME checks out, so live sending never drops while you migrate the record. ## Next steps - [Sending domains](/docs/guides/email/sending-domains) — registering a domain and the verification lifecycle - [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc) — every record on the domain and what it proves - [Tracking domain](/docs/guides/email/tracking-domain) — the optional branded hostname for open/click tracking - [Per-provider DNS walkthroughs](/docs/knowledge-base/dns-and-domains/generic-registrar) — publishing the CNAME in your registrar --- title: "Categories" description: "How the marketing and transactional categories classify your email and drive suppression policy, and why marketing sends must set the category explicitly." source: https://bird.com/en-us/docs/guides/email/categories --- # Categories Every email you send through Bird belongs to a category — a classification of _why_ you are sending, set with the `category` field on [`POST /v1/email/messages`](/docs/api/reference/create-email-message). The category does not change how the message is built or delivered; it changes which [suppression](/docs/guides/email/suppressions) records are allowed to block it. A recipient who unsubscribed from your newsletter has told you something about marketing mail, not about password resets — categories are how Bird tells those two intents apart. There are two built-in categories in v1, and they are aliases rather than resources you create: you reference them by name on each send, and there is no category-management API. ## The two categories - **`marketing`** — mail the recipient chose to receive and can choose to stop receiving: marketing campaigns, newsletters, product announcements, promotional offers. Anything where "I don't want this anymore" is a valid and respected answer. - **`transactional`** — mail the recipient's own action requires: password resets, email verification, one-time codes, order receipts, security and account alerts. The recipient needs these to use your product, regardless of their marketing preferences. The test is not how the message looks but what happens if it doesn't arrive. If a missing message breaks a flow the recipient initiated, it is transactional; if it is something you initiated to reach them, it is marketing. ## How the category drives suppression policy Each suppression record carries an `applies_to` policy, and the category decides whether that policy matches the send. From the category's point of view, the [suppression matrix](/docs/guides/email/suppressions) reads like this: | Suppression reason | `marketing` category | `transactional` category | | ------------------ | -------------------- | ------------------------ | | `hard_bounce` | Blocked | Blocked | | `complaint` | Blocked | Allowed | | `unsubscribe` | Blocked | Allowed | | `manual` | Blocked | Blocked | The asymmetry is deliberate. `hard_bounce` means the address does not exist — sending is pointless in any category — and `manual` is a deliberate decision by your team that Bird never second-guesses, so both block everything. `complaint` and `unsubscribe` are preference signals about unwanted mail: they block the `marketing` category, but a recipient who reported your newsletter as spam still needs their password reset to arrive, so `transactional` sends deliver through them. Suppressed recipients in either category are rejected visibly, never silently dropped — each one appears in the message's recipient list with an inspectable reason. See [suppressions](/docs/guides/email/suppressions) for the full behavior. ## The default — and why marketing sends must opt in When you omit `category` on a single send, a batch, or a broadcast, it defaults to **`marketing`**. Set `category: "transactional"` explicitly for operational mail like receipts and password resets, so it is not gated by an unsubscribe. That default makes the common mistake an easy one: marketing content sent without an explicit category rides the transactional policy, which means complaints and unsubscribes do _not_ stop it. Mailing people who have unsubscribed or reported spam is both a deliverability risk and, in many jurisdictions, a compliance problem. Always set `category: "marketing"` explicitly on marketing, newsletter, and announcement sends so those preference signals are honored: ```json { "from": "news@yourdomain.com", "to": ["subscriber@example.com"], "subject": "June product update", "html": "<p>What's new this month...</p>", "category": "marketing" } ``` Relying on the default is fine for the mail the default is named after — receipts, resets, verification — and wrong for everything else. ## Categories as an analytics dimension The category is preserved on the message and returned wherever the message appears: the send response includes `category`, and reads of the message return it. That makes it a clean dimension for slicing — separating marketing and operational mail in your delivery, bounce, and engagement numbers without tagging every send yourself. In the dashboard, the [email log](/docs/guides/email/email-log) filters by category, and the API returns `category` on every message, so you can derive the split wherever you report. ## No category management in v1 `marketing` and `transactional` are the complete set: there is no API to create, rename, or configure categories, and no per-category settings to manage. The field is a classification on each send, validated against those two values. If you need finer-grained segmentation today, use [tags](/docs/guides/email/sending-email) alongside the category. ## Next steps - [Sending email](/docs/guides/email/sending-email) — the full `POST /v1/email/messages` payload, including `category` - [Suppressions](/docs/guides/email/suppressions) — the suppression list, the four reasons, and how addresses get added - [API reference](/docs/api/reference/create-email-message) — full request and response schemas --- title: "Contacts" description: "Store the people you email as contacts with typed custom properties, import them in bulk, and keep them in sync with your own records." source: https://bird.com/en-us/docs/guides/email/contacts --- # Contacts A contact is a stored recipient: an email address plus whatever you know about the person behind it. Contacts are where Bird keeps your recipient data so you can group people into [audiences](/docs/guides/email/audiences) and reuse them. You create and manage them in the Email app and from the `bird` [CLI](/docs/cli). ## Reaching contacts Storing a contact does not send anything on its own. To email one person, send a normal message to their address with the [send API](/docs/guides/email/sending-email); the contact record just keeps who they are, ready to reuse. To reach many at once, send a [batch](/docs/guides/email/sending-bulk); to build reusable groups, see [audiences](/docs/guides/email/audiences). ## The Contacts page The [**Contacts**](https://bird.com/dashboard/w/email/contacts) page (**Email → Contacts**) lists everyone in your workspace, with their email, name, and external ID. Search by email address to find one, click a row to open the contact, and use the header buttons to add a single contact or import many at once. Viewing contacts needs the `email_marketing` read permission; adding, editing, and deleting need write. ![The Contacts page in the Bird dashboard, listing stored contacts by email, name, and external ID, with search and Add contact controls](/images/docs/dashboard-email-contacts.png) ## What a contact holds Every contact has a workspace-unique email address and, optionally, a name and your own identifier for it: | Field | What it is | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `email` | The address, required and unique per workspace. Bird stores it trimmed and lowercased, so `Sam@Acme.com` and `sam@acme.com` are the same contact. | | `first_name` | Optional given name, used to personalize a send. | | `last_name` | Optional family name. | | `external_id` | Optional. Your own primary key for the person (a user ID from your database), unique per workspace when set. It is how you match a Bird contact back to your own records without relying on the email. | | `data` | Custom property values, one per registered contact property. See below. | Each contact also carries a Bird ID with a `con_` prefix and its created and updated timestamps. ## Contact properties Contact properties are the typed schema for the custom fields on a contact. You register a property once per workspace, and from then on every contact can carry a value for it under `data`. Registering the schema up front is what lets you personalize and segment reliably: a value is always the type you declared, not an untyped blob that varies per contact. ![The Contact properties page in the Bird dashboard, listing registered properties with their key, type, and fallback value, including one archived property marked with an Archived badge](/images/docs/dashboard-email-contact-properties.png) Manage them on the [**Contact properties**](https://bird.com/dashboard/w/email/contact-properties) page, reached from the **Properties** button on the Contacts page. Each property has a key, a type, and an optional fallback: - **Key** is the name you reference the value by, for example `plan_tier`. It must be lowercase and start with a letter (`^[a-z][a-z0-9_]*$`), and it is fixed once created. - **Type** is one of `string`, `number`, or `boolean`, and it is also fixed once created. There is no dedicated date type; store a date as a `string` (an ISO 8601 date, say). A value written to a contact must match its property's declared type. - **Fallback value** is the default used when a contact has no value for the property, so a missing `first_name` reads as "there" rather than a blank. Properties are not deleted, they are archived. Archiving a property stops new writes to its key while keeping every value already stored, and the key stays reserved so it can never be reused for a different type. Unarchive it to bring it back. This is why the type is immutable: a stored `number` must never start being read as a `string`. One thing the dashboard does not do is let you type a custom value directly on a contact. Property _values_ arrive through import or the CLI, keyed by the property names you defined here. The contact page shows them; it does not edit them field by field. ## Importing and syncing contacts The dashboard import is the fastest way to load a list. From **Import** on the Contacts page, paste up to 1000 rows, one contact per line as `email, first name, last name` with only the email required. Bird matches each row to an existing contact by email and updates it, or creates it if it is new, so re-importing the same file is an upsert rather than a pile of duplicates. The result tells you how many were added, updated, and skipped. To sync from your own database, script the CLI instead. `bird contacts create <email>` adds one; `bird contacts batch` upserts a whole file of them in a single call, which is how you keep Bird in step with your system one batch per run rather than one request per person. A batch can also carry custom property values and drop the contacts straight into audiences as it creates them. ```bash bird contacts create alex@example.com --first-name Alex --last-name Rivera --external-id user_8412 ``` Set your own `external_id` on each contact so a later sync can find the same person even if their email changes. ## Deleting a contact Deleting a contact is a hard delete: the record and its audience memberships go, and it is not recoverable. It does not touch suppressions, though. An address that unsubscribed or hard-bounced stays on your [suppression list](/docs/guides/email/suppressions) even after you delete the contact, so deleting someone never quietly makes them mailable again. ## Next steps - [Audiences](/docs/guides/email/audiences) — group contacts into reusable lists - [Suppressions](/docs/guides/email/suppressions) — the workspace list of addresses Bird will not deliver to, kept separate from your contacts - [Bulk sending](/docs/guides/email/sending-bulk) — reaching many recipients today with batches - [CLI](/docs/cli) — scripting contacts, properties, and audiences with the `bird` command --- title: "Dedicated IPs & pools" description: "How dedicated IPs isolate your sending reputation, how IP pools group them, and how to choose a pool per send, with the shared Bird pool as the default." source: https://bird.com/en-us/docs/guides/email/dedicated-ips-and-pools --- # Dedicated IPs & pools Buy dedicated IPs, group them into pools, and choose a pool per send with the `ip_pool` field. On shared infrastructure, your email leaves from IP addresses you share with other Bird customers — great reputation management on our side keeps those IPs healthy, but your reputation is still pooled with everyone else's. A **dedicated IP** is an address only you send from, so mailbox providers judge your traffic on your own track record alone. Dedicated IPs are a paid add-on, and they make sense once you send consistent volume (roughly 100k+ emails per month); below that, the shared infrastructure's established reputation will outperform a lightly-used dedicated IP. You never need dedicated IPs to send. You start with the **Shared Bird.com Pool** — Bird's shared sending infrastructure, surfaced in your pool list as a protected pool — as your default, so you can send email from day one without creating or configuring anything. Dedicated IPs and pools are opt-in on top of that. New dedicated IPs need a [warmup period](/docs/guides/email/ip-warmup) of roughly 30 days before they can carry full volume — Bird manages warmup automatically and routes overflow through the shared pool in the meantime. Dedicated IPs and pools are owned by your organization and shared across your workspaces — relevant only if you have more than one; with a single workspace it's transparent. ## Buy a dedicated IP Purchase IPs with `POST /v1/organization/dedicated-ips`. Purchases are bounded by your plan's `max_dedicated_ips` quota. ```bash curl -X POST https://us1.platform.bird.com/v1/organization/dedicated-ips \ -H "Authorization: Bearer $BIRD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "quantity": 1 }' ``` On your **first** purchase you can omit `pool_id`: Bird auto-creates your first pool, named "Dedicated IPs" (you can rename it like any other pool), and provisions the IPs into it. Once you have at least one pool, `pool_id` is **required** — you choose where each new IP lands: ```bash curl -X POST https://us1.platform.bird.com/v1/organization/dedicated-ips \ -H "Authorization: Bearer $BIRD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "quantity": 2, "pool_id": "ipp_01krdgeqcxet5s7t44vh8rt9mg" }' ``` The response is the list of provisioned IPs, each starting in `warming` status: ```json { "data": [ { "id": "dip_01krdgeqcxet5s7t44vh8rt9mg", "organization_id": "org_01krdgeqcxet5s7t44vh8rt9mg", "address": "192.0.2.10", "hostname": "mta123.sparkpostmail.com", "status": "warming", "warmup_progress": 0, "pool_id": "ipp_01krdgeqcxet5s7t44vh8rt9mg", "purchased_at": "2026-06-10T14:30:00Z", "created_at": "2026-06-10T14:30:00Z", "updated_at": "2026-06-10T14:30:00Z" } ] } ``` Buying an IP does **not** change your default pool. The shared pool stays the default until you explicitly switch it — routing all your unspecified traffic onto a cold, still-warming IP would hurt your deliverability, so Bird never does it for you. You can track each IP's status and warmup progress on the [**Dedicated IPs**](https://bird.com/dashboard/w/email/ip-pools/dedicated-ips) page (**Email → IP Pools → Dedicated IPs**) or via `GET /v1/organization/dedicated-ips`. ![The Dedicated IPs view in the Bird dashboard, showing a purchased IP in warming status with its pool and warmup progress](/images/docs/dashboard-email-dedicated-ips.png) | Status | Meaning | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `warming` | The IP is new and warming up. It carries a growing share of your traffic. | | `active` | Warmup complete. The IP handles full volume. | | `suspended` | Reserved for a temporarily suspended IP. Bird does not place IPs in this status today, so you will not see it in practice. | | `pending_cancellation` | You cancelled the IP. It stops counting toward its pool's sendable IPs and is deprovisioned shortly after. | ## IP pools Pools group dedicated IPs so you can separate sending streams — a common setup is one pool for transactional mail and another for marketing, so a campaign's reputation never touches your password resets. Pools are managed via the IP pools API or the [**IP Pools**](https://bird.com/dashboard/w/email/ip-pools) page (**Email → IP Pools**) in the dashboard. ![The IP Pools view in the Bird dashboard, listing a custom pool with a warming IP alongside the protected Shared Bird.com Pool marked as default](/images/docs/dashboard-email-ip-pools.png) Create a pool: ```bash curl -X POST https://us1.platform.bird.com/v1/organization/ip-pools \ -H "Authorization: Bearer $BIRD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Transactional" }' ``` ```json { "id": "ipp_01krdgeqcxet5s7t44vh8rt9mg", "organization_id": "org_01krdgeqcxet5s7t44vh8rt9mg", "name": "Transactional", "is_default": false, "protected": false, "ips": [], "created_at": "2026-06-10T14:30:00Z", "updated_at": "2026-06-10T14:30:00Z" } ``` Pools are always created non-default and empty; you put IPs in them either by purchasing directly into them (`pool_id` on the purchase) or by moving existing IPs in. If you run more than one workspace, pools are shared across them: `GET /v1/organization/ip-pools` returns the same list whichever workspace API key calls it. Your pool list always contains the **Shared Bird.com Pool** with the well-known ID `ipp_shared`. It is `protected: true`: it cannot be renamed or deleted, it cannot hold dedicated IPs, and it does not count against your pool quota. The only thing you can change about it is whether it is the default. ### Moving an IP between pools A dedicated IP is **always in exactly one pool** — there is no unassigned state and no unassign operation. To move an IP, assign it to its new pool: ```bash curl -X POST https://us1.platform.bird.com/v1/organization/dedicated-ips/dip_01krdgeqcxet5s7t44vh8rt9mg/assign \ -H "Authorization: Bearer $BIRD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "pool_id": "ipp_01krdgf0bqe9x8m2v44vh8rt9h" }' ``` The shared pool is not a valid assign target — dedicated IPs only live in your own pools. ### Deleting a pool **Only empty pools can be deleted.** A `DELETE /v1/organization/ip-pools/:id` on a pool that still contains IPs is rejected with a `422` — move or cancel its IPs first. The current default pool cannot be deleted at all; designate another default before deleting it. ## The default pool When a send does not specify a pool, Bird uses your **default pool**. The rules around the default are strict, because the default is what carries every send that doesn't choose: - **There is always exactly one default.** The shared pool is the initial default, so the invariant holds from day one. - **The default is moved, never cleared.** You change the default by setting `is_default: true` on another pool (`PATCH /v1/organization/ip-pools/:id`), which atomically unsets the previous one. Setting `is_default: false` on the current default is rejected with a `422` — to go back to shared routing, set the shared pool as the default instead: ```bash curl -X PATCH https://us1.platform.bird.com/v1/organization/ip-pools/ipp_shared \ -H "Authorization: Bearer $BIRD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "is_default": true }' ``` - **A dedicated pool needs at least one IP that isn't winding down to become the default.** A `warming` IP counts, so a pool whose only IP is still warming up can be made the default; only IPs in `pending_cancellation` are excluded — your default can never be backed solely by IPs that are on their way out. The shared pool is exempt: it is always a valid default. - **The last remaining IP cannot leave the default pool.** You can neither cancel it nor move it to another pool while its pool holds the default designation — switch the default away first, then drain the pool. Together with the previous rule, this means your default pool can never end up with nothing to send from. ## Selecting a pool at send time Choose a pool per email with the optional `ip_pool` field on [`POST /v1/email/messages`](/docs/guides/email/sending-email): ```bash curl -X POST https://us1.platform.bird.com/v1/email/messages \ -H "Authorization: Bearer $BIRD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "noreply@acme.com", "to": ["user@example.com"], "subject": "Your receipt", "html": "<p>Thanks for your order.</p>", "ip_pool": "ipp_01krdgeqcxet5s7t44vh8rt9mg" }' ``` Resolution works like this: - **`ip_pool` omitted** — the send uses your default pool (the shared pool, unless you changed it). - **`ip_pool: "ipp_shared"`** — the send is forced through the shared pool, regardless of your default. Useful for routing a low-stakes stream through shared infrastructure while a dedicated pool is your default. - **`ip_pool` set to one of your pool IDs** — the send goes through that pool. An `ip_pool` value that is unknown, belongs to another organization, or names a pool with no IPs available to send from is **rejected with a `422`** — Bird never silently downgrades the send to the shared pool. If you asked for dedicated routing, you either get it or you get an explicit error you can handle. An IP in `pending_cancellation` no longer counts toward its pool's sendable IPs, so if it was the pool's only IP, sends through that pool start failing with a `422` once it is cancelled. Cancel an IP only after you have another sendable IP in the pool, or after moving your traffic to a different pool. ## Cancelling a dedicated IP Cancellation is two-phase — `DELETE` schedules it rather than removing the IP immediately: ```bash curl -X DELETE https://us1.platform.bird.com/v1/organization/dedicated-ips/dip_01krdgeqcxet5s7t44vh8rt9mg \ -H "Authorization: Bearer $BIRD_API_KEY" ``` The IP transitions to `status: pending_cancellation` and gets a `cancels_at` timestamp. It stays in its pool until Bird deprovisions it, which happens shortly after cancellation. A cancelled IP stops counting toward its pool's sendable IPs straight away, so plan to switch any traffic off it before you cancel. Cancelling an IP that is already pending cancellation has no further effect. Remember the default-pool guard: the last remaining IP in the default pool cannot be cancelled — move the default designation (to the shared pool or another pool) first. ## Next steps - [IP warmup](/docs/guides/email/ip-warmup) — how new dedicated IPs ramp up to full volume over ~30 days - [Sending email](/docs/guides/email/sending-email) — the full `POST /v1/email/messages` request, including `ip_pool` - [Dedicated IPs](https://bird.com/dashboard/w/email/ip-pools/dedicated-ips) and [IP pools](https://bird.com/dashboard/w/email/ip-pools) are provisioned and managed from the dashboard (these management endpoints are not part of the public API reference) --- title: "Deliverability · Email" description: "How the shared reputation model applies to email on Bird: domain authentication, dedicated IPs and warmup, and reputation monitoring, with links to each." source: https://bird.com/en-us/docs/guides/email/deliverability --- # Deliverability · Email This page maps the [shared deliverability model](/docs/guides/deliverability) onto email: which records prove your identity, how dedicated IPs isolate your reputation, and where to watch for trouble. Each section links to the page that covers the mechanics in full — go there for the record shapes, API payloads, and edge cases. ## Prove your identity: domain authentication Email authentication on Bird is per organization, even on a shared domain: Bird generates a DKIM signing key for your organization and publishes its public half under a selector unique to you, so your authentication proof is yours alone no matter who else sends from the same domain. Alongside DKIM, you publish a return-path (envelope-from) CNAME that routes bounces through Bird and gives you SPF alignment without touching your apex, and a DMARC record that states your policy for mail failing those checks. **The send gate is DKIM + return-path + DMARC.** A domain sends only once all three verify. The fourth record — the tracking CNAME for branded open and click tracking — is separate and optional: it gates branded tracking, never sending. - [Sending domains](/docs/guides/email/sending-domains) — adding a domain and the verification lifecycle - [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc) — what each record proves, exact shapes, and why apex SPF is not required - [Knowledge base: DNS setup at your registrar](/docs/knowledge-base/dns-and-domains/generic-registrar) — step-by-step instructions per DNS provider ## Isolate your reputation: dedicated IPs and pools By default your mail rides the **Shared Bird.com Pool** — Bird's shared sending infrastructure, present in every organization's pool list as a protected pool, and your default until you explicitly flip it. Dedicated IPs are the opt-in alternative: addresses only your organization sends from, organized into organization-scoped pools that every workspace can use. Two rules keep a cold IP from hurting you: - **New IPs warm automatically over roughly 30 days.** Each purchased IP starts in `warming` status with a `warmup_progress` of `0`–`100`; while it ramps, traffic beyond what it can safely carry overflows through the shared pool, so your volume never drops. - **Your default pool can never be empty.** A dedicated pool needs at least one live IP before it can become the default -- a still-`warming` IP counts, since traffic beyond what it can carry overflows through the shared pool, but a pool whose only IPs are already in `pending_cancellation` cannot hold the default. Per send, choose a pool with the optional `ip_pool` field on the send request; omit it to use your default. - [Dedicated IPs & pools](/docs/guides/email/dedicated-ips-and-pools) — purchasing, pool management, default-pool rules, and `ip_pool` routing - [IP warmup](/docs/guides/email/ip-warmup) — the warmup schedule, monitoring `warmup_progress`, and when to flip your default ## Watch your reputation: bounces, complaints, suppressions Your reputation signal arrives as events: bounce and complaint events tell you which addresses are failing and who is reporting you, and Bird's suppression list automatically stops repeat sends to hard bounces, complainers, and unsubscribes before they leave the platform. Aggregate delivery, open, and click rates are on the [**Metrics**](https://bird.com/dashboard/w/email/metrics) page in the dashboard — a falling delivery rate or rising complaint rate is your earliest warning. To dig into a specific send, open it in the [email log](/docs/guides/email/email-log); if you also want the SPF, DKIM, and DMARC verdicts the receiving mailbox recorded, paste the received copy's raw source into the [Email header analyzer](/tools/email-analyzer). Two adjacent things to know: [BIMI](/docs/guides/email/bimi) (your logo in supporting inboxes) is a DNS standard you publish yourself — there is no BIMI API in v1 — and there is no blocklist-status API in v1 either. - [Suppressions](/docs/guides/email/suppressions) — what lands on the list automatically and how to manage it - [Tracking & metrics](/docs/guides/email/tracking-and-metrics) — open/click instrumentation and the aggregate view --- title: "DKIM, SPF & DMARC" description: "What each DNS record on a Bird sending domain proves, the exact record shapes, why apex SPF is not required, and how Bird verifies your records." source: https://bird.com/en-us/docs/guides/email/dkim-spf-dmarc --- # DKIM, SPF & DMARC Email authentication is how receiving mail servers decide whether a message really came from your domain. Each DNS record you publish for a sending domain proves one specific thing: DKIM proves the message was signed by you, the return-path proves bounces flow back through an address aligned with your domain, and DMARC tells receivers what to do when those checks fail. This page describes each record Bird asks you to publish, what it proves, and how verification works. If you have not added a sending domain yet, start with [Sending domains](/docs/guides/email/sending-domains). ## The DNS records Open your domain from the [**Domains**](https://bird.com/dashboard/w/email/domains) page to see its detail page, which shows the full set of records to publish, with copyable host and value fields. The same set is returned by the API. ![A domain's DNS Records page in the Bird dashboard, showing the verified DKIM record with copyable name and value, and the return-path and DMARC sections below](/images/docs/dashboard-email-domain-detail.png) Three records gate sending: DKIM, the return-path CNAME, and DMARC. The tracking CNAME is optional and only gates branded open/click tracking, not sending itself. ### DKIM (TXT) DKIM is your ownership and signing proof. Bird generates a signing key for your organization and signs every message you send with it; the public half is published as a TXT record under a selector that is unique to your organization. Receivers fetch the public key from that selector and check the signature, which proves the message was sent by someone with control of your domain's DNS — and because each organization on Bird gets its own selector and key, your DKIM proof is yours alone, even if another Bird customer sends from the same physical domain. | Type | Host | Value | | ---- | ----------------------------------- | -------------------------------- | | TXT | `<selector>._domainkey.example.com` | `v=DKIM1; k=rsa; p=<public-key>` | The selector and public key are generated for you — copy the exact host and value from the dashboard or the API rather than constructing them by hand. Long keys may be shown split into multiple quoted strings; that is standard DNS formatting and most providers accept the value as shown. ### Return-path (CNAME) The return-path record sets your envelope-from (bounce) domain. Bounces and delivery feedback for your messages are addressed to this hostname, and pointing it at Bird lets us process them for you. It is also what gives you SPF alignment: receivers evaluate SPF against the envelope-from domain, and because this hostname resolves to Bird's bounce infrastructure, SPF passes and aligns with your domain without any record at your apex (see [Where's SPF?](#wheres-spf) below). | Type | Host | Value | | ----- | ------------------ | -------------------------- | | CNAME | `send.example.com` | `<region>.bounce.bird.com` | The host defaults to `send.` under your sending domain but is your choice per workspace — each workspace using a shared domain can pick its own return-path hostname (or share one). The value depends on the region your workspace sends from; copy it from the dashboard. ### DMARC (TXT) DMARC publishes your policy: it tells receivers what to do with mail that fails DKIM or SPF alignment (`p=none` to just monitor, `p=quarantine` or `p=reject` to enforce), and where to send aggregate reports (`rua`). Bird requires a DMARC record to exist before the domain can send, and verifies it by resolving your DNS directly — a record at the domain itself or an inherited record at a parent domain both count. | Type | Host | Value | | ---- | -------------------- | ---------------------------------------------------------- | | TXT | `_dmarc.example.com` | `v=DMARC1; p=none; rua=mailto:example.com@dmarc.bird.com;` | The value above is Bird's recommendation: `p=none` is a safe starting policy, and the `rua` address routes aggregate reports to Bird. You can use your own policy and reporting address — the gate only requires that a valid DMARC record exists — the [DMARC policy generator](/tools/dmarc-policy) can help you write one. If you already have a DMARC record (or one at a parent domain), you do not need to change it. If you route `rua` reports to your own mailbox instead, the [DMARC report analyzer](/tools/dmarc-report-analyzer) turns the raw XML into something readable. ### Tracking (CNAME, optional) The tracking record gives you a branded hostname for open and click tracking. When click tracking is enabled, links in your messages are rewritten to this hostname instead of a generic Bird domain, which looks better to recipients and keeps link reputation tied to your brand. It is per workspace, like the return-path. | Type | Host | Value | | ----- | ------------------- | ------------------------- | | CNAME | `links.example.com` | `<region>.links.bird.com` | This record is not part of the send gate — a domain with verified DKIM, return-path, and DMARC can send even if the tracking record is missing. It only gates whether branded open/click tracking is available. ## Where's SPF? You do not need to publish an SPF record at your domain's apex (`example.com`) for Bird, and the dashboard will not ask you to. This surprises people coming from providers that require an `include:` in your apex SPF record, so it is worth being explicit about why: SPF is evaluated against the envelope-from domain, not the visible From address. Your envelope-from is the return-path hostname (`send.example.com`), and the verified return-path CNAME points it at Bird's bounce infrastructure — which carries the SPF authorization. SPF passes, and it aligns with your domain because the return-path is a subdomain of it. Adding `include:` entries at your apex would do nothing for your Bird mail, and apex SPF records have a hard limit of 10 DNS lookups that is easy to exhaust, so not consuming any of it is a feature. If you have an existing apex SPF record for other senders, leave it alone — it neither helps nor hurts your Bird setup. ## How verification works Bird verifies your records by checking DNS — there is no callback from your DNS provider, so verification is detection, not notification: - **Right after you add a domain**, Bird polls frequently — checks start within seconds and back off gradually over the first 72 hours. DNS records usually propagate in seconds to minutes, so most domains verify shortly after you publish the records. You can also trigger an immediate re-check from the dashboard or via the API. - **After that**, a daily sweep re-checks every domain — pending domains that you fixed late, verified domains, and failed ones alike. If you publish the records on day four, the sweep picks them up automatically; you never need to re-add the domain. - **A record marked invalid** means Bird resolved your DNS and found a wrong value — fix the record, then re-verify from the dashboard or wait for the sweep. A domain that simply stays `pending` means the records were not found yet, which is the normal state before you publish them. - **Downgrades are deliberate.** If a verified domain's records disappear, Bird does not knock it offline on a single failed check — a transient DNS blip will not interrupt your sending. Only consecutive failed daily checks downgrade the domain, at which point sending is gated again until the records are restored. The domain's `status` and per-record state are available at any time from the domain detail endpoint, and verification can be triggered on demand — see the [Domains API reference](/docs/api). ## Next steps - Add and manage domains end to end: [Sending domains](/docs/guides/email/sending-domains) - Step-by-step instructions for your DNS provider, for example [Cloudflare](/docs/knowledge-base/dns-and-domains/cloudflare) — guides for other registrars are in the same knowledge-base section - Verify endpoints and record payloads: [Domains API reference](/docs/api) --- title: "Email log" description: "Browse, filter, and inspect every message your workspace sent from the Emails page: statuses, the per-recipient event timeline, and the rendered preview." source: https://bird.com/en-us/docs/guides/email/email-log --- # Email log The **email log** — the [**Emails**](https://bird.com/dashboard/w/email/emails) page in the Bird dashboard — is your workspace's record of every message it has sent: every send made through [`POST /v1/email/messages`](/docs/guides/email/sending-email) lands here, newest first. Use it to confirm a specific message went out, see where a recipient is in the delivery lifecycle, and read the exact content that was sent. It is the per-message companion to the aggregate [Metrics](/docs/guides/email/tracking-and-metrics) page — Metrics tells you the rates across everything you send, the email log lets you drill into a single message. For a tour of where this page sits in the dashboard, see the [dashboard tour](/docs/knowledge-base/getting-started/dashboard-tour). The page's **Receiving** tab is the same log for mail Bird receives — that side is covered in [Receiving email](/docs/guides/email/receiving-email). ![The Emails page in the Bird dashboard: a table of sent messages with status, sender, recipient, subject, category, and sent time, plus a recipient search and status, category, and date filters](/images/docs/dashboard-email-emails.png) ## The message list Each row is one message. The columns are: | Column | What it shows | | :----------- | :-------------------------------------------------------------------------------------------------------- | | **Status** | The message's current delivery status (see [Statuses](#statuses) below), shown as a color-coded indicator | | **From** | The sender — display name and address, or just the address | | **To** | The primary recipient; sends with more than one recipient show a count you can hover to expand | | **Subject** | The message subject (click it, or anywhere on the row, to open the message) | | **Category** | The message [category](/docs/guides/email/categories) — `TXN` for transactional | | **Sent** | When the send was accepted, as a relative time (hover for the exact timestamp) | Rows whose status needs attention are tinted: a warning tint for `Deferred`, `Bounced`, and `Partial failure`, and a stronger tint for `Complained` and `Rejected`. The list is paginated; use **Prev** and **Next** to move between pages. ## Searching and filtering A message log fills up fast, so the page leads with one search box and three filters. They combine — a status filter plus a date range narrows to messages matching both. **Search by recipient.** The search box matches an **exact recipient email address** — type the full address you sent to (e.g. `jane@example.com`) to find every message addressed to it. It is an exact match, not a substring search, so partial addresses or domains won't return results. **Status.** Filter to a single delivery status. The options are the same statuses listed below: `Accepted`, `Processed`, `Deferred`, `Delivered`, `Partial failure`, `Bounced`, `Complained`, and `Rejected`. **Category.** Filter by message category. `Transactional` is available today; `Marketing` is coming soon. **Date.** Filter by when the message was sent — pick a preset (**Last 3 days**, **Last 7 days**, **Last 30 days**) or choose a custom range from the calendar. When a filter combination matches nothing, the page shows a **No emails match your filters** state with a **Clear filters** action to reset back to the full list. ## Statuses A message's status is the roll-up of where its recipients are in the [delivery lifecycle](/docs/guides/email/events). For a single-recipient message it mirrors that recipient's status directly; for a multi-recipient send it reflects the message as a whole — **Partial failure** means some recipients succeeded and some did not. | Status | Meaning | | :------------------ | :------------------------------------------------------------------------------------------------------------------------- | | **Accepted** | Bird has the send and is preparing to deliver it | | **Processed** | The message is built and queued for delivery | | **Delivered** | The recipient's mail server accepted the message | | **Deferred** | A temporary failure; Bird is retrying — resolves to delivered or bounced | | **Bounced** | A permanent failure; the recipient's mail server refused the message | | **Complained** | The recipient reported the message as spam | | **Rejected** | The send was rejected before delivery was attempted — most often a [suppressed](/docs/guides/email/suppressions) recipient | | **Partial failure** | A multi-recipient send where outcomes differ across recipients | The indicator is color-coded: green for delivered, blue for in-flight (`Accepted`, `Processed`), a warning color for `Deferred`, `Bounced`, and `Partial failure`, and a destructive color for `Complained` and `Rejected`. ## Inspecting a message Click any row to open the message in a side panel. It has four tabs — **Events**, **Details**, **HTML**, and **Text** — and arrows to step to the previous or next message in the list without closing it. ![The email detail side panel in the Bird dashboard, showing the per-recipient events timeline with accepted, processed, delivered, opened, and clicked events alongside their timestamps](/images/docs/dashboard-email-detail.png) ### Events The default tab is a per-recipient timeline of everything that happened to the message, in order, each with its timestamp. This is the same event stream described in the [email events reference](/docs/guides/email/events), rendered for one message: `Accepted` → `Processed` → `Delivered`, then engagement events like `Opened` and `Clicked`, or failure events like `Bounced`, `Deferred`, `Complained`, and `Rejected`. - For a send with several recipients, an **All recipients** selector lets you view the merged timeline or filter to one recipient's journey. - Failure events carry their detail inline — a bounce shows its description, a rejection shows its reason. - An `Opened` event auto-fetched by an inbox privacy proxy is flagged as not a real open, matching how [open tracking](/docs/guides/email/tracking-and-metrics#open-tracking) treats prefetches. - When an event auto-adds the recipient to your [suppression list](/docs/guides/email/suppressions) — a hard bounce or a complaint — the timeline says so and links to the list. - The timeline refreshes on its own while a message is still in flight, so opens and clicks appear as they happen. If a delivered message has no opens or clicks, the timeline explains why — most commonly that [open and link tracking](/docs/guides/email/tracking-and-metrics) wasn't enabled for the domain or wasn't requested on the send. ### Details The **Details** tab is the message's metadata: its **Message ID**, the **From**, **To**, and any **CC**, **BCC**, or **Reply-To** addresses, the sending **Domain**, the **Category**, whether **Open tracking** and **Link tracking** were enabled, and the **Sent** and **Delivered** timestamps. Any [tags and metadata](/docs/guides/email/sending-email) you set on the send are listed here too. Most fields can be copied with one click. ### HTML and Text The **HTML** tab renders the message exactly as it was sent — a **Preview** sub-tab shows the message in a sandboxed frame, and a **Source** sub-tab shows the raw HTML. The **Text** tab shows the plain-text body. Content is stored shortly after the send, so for a just-sent message these tabs may briefly show that the content is still being stored. ## Next steps - [Tracking & metrics](/docs/guides/email/tracking-and-metrics) — aggregate stats across everything you send, and how open and click tracking work - [Email events reference](/docs/guides/email/events) — the full per-recipient event vocabulary the timeline is built from - [Suppressions](/docs/guides/email/suppressions) — why a recipient was rejected and how to manage the suppression list - [Sending email](/docs/guides/email/sending-email) — the send payload, including tags, categories, and tracking toggles - [API reference](/docs/api) — read messages and their events programmatically with `GET /v1/email/messages` --- title: "Email events reference" description: "The catalog of email events Bird emits — lifecycle, failure, and engagement — with per-event payload fields and bounce classification." source: https://bird.com/en-us/docs/guides/email/events --- # Email events reference Every recipient on a send moves through a lifecycle, and Bird emits an event at each step. Events are per-recipient facts: a send to three addresses produces three independent event streams, each correlated by `email_id` + `recipient_id`. This page is the full event vocabulary; how events are delivered to your endpoint — signatures, retries, replay — is covered in the [Webhooks guide](/docs/guides/webhooks). The delivery lifecycle, as a path through the event types: - `email.accepted` — Bird has the send and is preparing to deliver - → `email.processed` — the message is built and queued for delivery - → `email.delivered` — the recipient's mail server accepted it - → or `email.deferred` — temporary failure; Bird retries, ending in `email.delivered` or `email.bounced` - → or `email.bounced` — permanent failure; the recipient's terminal status is `bounced` - → or `email.rejected` — the recipient was rejected before delivery was attempted (e.g. suppressed); distinct from a bounce - After `email.delivered`, the stream can continue with `email.out_of_band_bounce`, `email.complained`, `email.opened`, `email.clicked`, `email.unsubscribed`, and `email.list_unsubscribed` Each recipient ends in exactly one terminal delivery status — `delivered`, `bounced`, `complained`, or `rejected` — the per-recipient `status` returned by `GET /v1/email/messages/{message_id}/recipients`. (`GET /v1/email/messages/{message_id}` itself returns the aggregate message status and per-state counts.) Engagement events (`opened`, `clicked`, unsubscribes) and `out_of_band_bounce` never change that terminal status. ## The event envelope Events arrive at your webhook endpoint in the [Standard Webhooks](https://www.standardwebhooks.com) nested envelope described in the [Webhooks guide](/docs/guides/webhooks#event-catalog) — exactly three fields: `type`, `timestamp`, and a type-specific `data` object. The event's identity is not in the body: it rides in the `webhook-id` HTTP header, which is stable across retries of the same delivery and is your deduplication key. | Field | Description | | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | One of the event types on this page, e.g. `email.delivered` | | `timestamp` | When the event occurred (RFC 3339) — sort by this, never by arrival order, since deliveries are not ordered. Distinct from the `webhook-timestamp` header, which is the time of this delivery attempt and changes on every retry | | `data` | Event-specific payload (fields below) | For outbound email events, `data` always carries the identity base: `email_id`, `recipient_id`, `workspace_id`, the `recipient` address, and its envelope `recipient_role` (to/cc/bcc) — plus the `tags` and `metadata` you provided on the send, echoed on every event so you can route and correlate deliveries against your own records without an extra lookup (each is `null` when the send carried none). Within an event type the field set is stable — required fields, no surprises. ```json { "type": "email.delivered", "timestamp": "2026-06-10T14:30:00Z", "data": { "email_id": "em_01krdgeqcxet5s7t44vh8rt9mg", "recipient_id": "er_01krdgeqcxet5s7t44vh8rt9mg", "workspace_id": "ws_01krdgeqcxet5s7t44vh8rt9mg", "recipient": "user@example.com", "recipient_role": "to", "tags": [{ "name": "category", "value": "welcome" }], "metadata": { "order_id": "ord_123" } } } ``` Each event type's webhook `data` carries the fields listed for it below. The same per-recipient events are also queryable after the fact with `GET /v1/email/messages/{message_id}/events`, where each event carries an `id` (`ev_` prefix), `type`, `occurred_at`, `recipient_id`, and the same type-specific detail. Use the events API to backfill, replay, or reconcile against your webhook deliveries by `email_id` + `recipient_id`. A few fields are surfaced only through the events API (noted on the relevant event below), such as `is_prefetched` and `country`. ## Lifecycle events ### email.accepted Fires once per requested recipient when Bird accepts the send and begins preparing delivery. This is the first event in every recipient's stream. Webhook payload: the identity base only. ### email.processed Fires once per recipient when Bird has processed the message and queued it for delivery to the recipient's mail server. Webhook payload: the identity base only — to measure processing latency, compare this event's `timestamp` with `email.accepted`'s. ### email.delivered The recipient's mail server accepted the message. Webhook payload: the identity base only; the `sending_ip` Bird sent from — useful when investigating deliverability issues that correlate with specific IPs — is on the event via the events API. Delivery means the remote server took responsibility for the message; engagement events tell you what happened after. ### email.deferred A temporary delivery failure — the recipient's mail server asked Bird to try again later (mailbox full, greylisting, rate limiting). Bird retries automatically; a deferred recipient eventually resolves to `email.delivered` or `email.bounced`, so treat this event as informational, not terminal. Webhook payload: `bounce_type`, `bounce_class`, `defer_reason` (the reason the receiving server gave), and `sending_ip`. A recipient can defer multiple times before resolving. ## Failure events ### email.bounced A permanent delivery failure at SMTP time — the recipient's mail server refused the message. The recipient's terminal status becomes `bounced`. Webhook payload: the bounce classification — `bounce_type` (see the [classification table](#bounce-classification)), `bounce_class`, `bounce_code` (SMTP reply code, e.g. `550`), `bounce_description` (human-readable reason), and `sending_ip`. Hard bounces automatically [suppress](#auto-suppression) the address. ### email.out_of_band_bounce A late bounce notification that arrived asynchronously **after** the message was already delivered — the receiving server accepted the message at SMTP time, then sent a bounce report afterwards. It carries the same bounce classification as `email.bounced` (`bounce_type`, `bounce_class`, `bounce_code`, `bounce_description`, `sending_ip`), but it does **not** change the recipient's terminal status: the recipient stays `delivered`, and the out-of-band bounce is recorded as an additional fact on the timeline. Hard out-of-band bounces still trigger [auto-suppression](#auto-suppression), so future sends to the address are blocked even though this send's status is unchanged. ### email.rejected The recipient was rejected before the message ever reached the remote mail server — no delivery attempt was made. This is distinct from `email.bounced`: a bounce is the receiving server saying no, a rejection is the message never getting that far. Webhook payload: `rejection_reason` (also on the recipient record), one of: | `rejection_reason` | Meaning | | ---------------------- | --------------------------------------------------------------------------------------------------------------------- | | `recipient_suppressed` | The recipient is on the workspace [suppression list](/docs/guides/email/suppressions) — Bird did not attempt delivery | | `transmission_failed` | The message could not be transmitted for delivery | | `generation_failure` | The message could not be built for delivery (template or content issue) | | `policy_rejection` | The message was refused by sending policy | ### email.complained The recipient marked the message as spam, reported back to Bird through the mailbox provider's feedback loop. Complaints arrive after delivery and set the recipient's terminal status to `complained`. Webhook payload: `feedback_type` (the kind of report the provider sent, such as `abuse`). A complaint automatically [suppresses](#auto-suppression) the address — keep complaint rates low, since mailbox providers throttle senders whose mail gets reported. ## Engagement events Engagement events never change the recipient's delivery status — a recipient who opened is still `delivered`. ### email.opened The tracking pixel in the message body loaded. Webhook payload: `ip_address` and `user_agent` (when known). The events API additionally exposes `is_prefetched` and `country` (ISO 3166-1 alpha-2, derived from the client IP). **Check `is_prefetched` before counting opens**: it is `true` when the open was auto-fetched by an inbox privacy feature (Apple Mail Privacy Protection, Gmail's image proxy) rather than a real user action — counting prefetched opens inflates open rates. ### email.clicked The recipient clicked a tracked link. Webhook payload: `url` (the clicked URL), `ip_address`, and `user_agent` (when known). The events API additionally exposes `country`. A click is the strongest engagement signal — privacy prefetchers load pixels, but they rarely follow links. ### email.unsubscribed The recipient unsubscribed via a tracked unsubscribe link in the message body. Triggers [auto-suppression](#auto-suppression) for non-transactional mail. ### email.list_unsubscribed The recipient unsubscribed via the RFC 8058 one-click List-Unsubscribe mechanism — the unsubscribe button mailbox providers render in their own UI, driven by the message's `List-Unsubscribe` headers. Webhook payload: the identity base only — the mechanism is the event type itself. Distinct from `email.unsubscribed` so you can tell provider-rendered unsubscribes apart from clicks on your own footer link. Also triggers [auto-suppression](#auto-suppression) for non-transactional mail. ## The receiving event `email.received` is the one event about mail coming _into_ Bird rather than going out: it fires when Bird receives and parses an inbound message, and its payload carries the `inbound_message_id`, addressing, subject, and authentication verdicts. It belongs to the receiving pipeline, not the send lifecycle above — setup, payload, and the fetch-back API are covered in [Receiving email](/docs/guides/email/receiving-email). ## Bounce classification `bounce_class` is Bird's numeric bounce classification, carried on `email.bounced`, `email.out_of_band_bounce`, and `email.deferred` events. It rolls up into the coarse `bounce_type`, but keeps the fine-grained code so you can distinguish, say, a DNS failure from a spam block when both map to the same type: | `bounce_class` | `bounce_type` | Meaning | | ---------------------------- | -------------- | ---------------------------------------------------------------------------------------- | | `1` | `undetermined` | The receiving server's response was ambiguous | | `10`, `30` | `hard` | Permanent failure — invalid address or non-existent domain | | `20`–`24`, `40`, `70`, `100` | `soft` | Transient failure — mailbox full, server temporarily unavailable, DNS or routing trouble | | `25` | `admin` | Administrative refusal — relaying denied, blocklisted domain | | `50`–`54` | `block` | The receiving server blocked the sending IP for reputation reasons | Any class outside this list maps to `undetermined`. Only `hard` bounces trigger auto-suppression; `soft`, `block`, `admin`, and `undetermined` bounces do not, since the address may still be deliverable. ## Auto-suppression Three event classes automatically add the recipient to the workspace [suppression list](/docs/guides/email/suppressions). The `email_suppression.created` event is reserved for these additions but is not yet delivered to webhooks -- its payload is not finalized, so a subscription to it will not fire for now. Drive suppression handling off the underlying lifecycle events below until it ships: - **Hard bounces** (`email.bounced` or `email.out_of_band_bounce` with `bounce_type: "hard"`) — suppressed for all mail - **Spam complaints** (`email.complained`) — suppressed for non-transactional mail - **Unsubscribes** (`email.unsubscribed` or `email.list_unsubscribed`) — suppressed for non-transactional mail Subsequent sends to a suppressed address are rejected up front with `email.rejected` and `rejection_reason: "recipient_suppressed"` — they never count against your deliverability. ## Next steps - [Webhooks & events](/docs/guides/webhooks) — endpoint setup, signature verification, retries, and replay - [Webhooks API reference](/docs/api/reference/create-webhook) — full endpoint and schema documentation - [Suppressions](/docs/guides/email/suppressions) — how the suppression list works and how to manage it - [Unsubscribe links](/docs/guides/email/unsubscribe-links) — wiring up the unsubscribe paths behind `email.unsubscribed` and `email.list_unsubscribed` - [Testing & sandbox](/docs/guides/email/testing-sandbox) — sandbox sends emit real events through the normal delivery path, ideal for exercising your handler --- title: "IP warmup" description: "How Bird automatically warms up new dedicated IPs over roughly 30 days, how to monitor progress via the API, and why the shared pool stays your default." source: https://bird.com/en-us/docs/guides/email/ip-warmup --- # IP warmup A brand-new IP address has no sending history, and mailbox providers treat mail from an unknown IP with suspicion: sudden high volume from a cold IP looks exactly like a spammer spinning up infrastructure, so Gmail, Outlook, and the rest throttle or junk it. Warmup is the process of earning a reputation gradually — starting with a small volume and ramping up over weeks so providers learn to trust the address. Every [dedicated IP](/docs/guides/email/dedicated-ips-and-pools) you purchase from Bird goes through it before it can carry your full volume. You don't manage warmup yourself. Every newly purchased dedicated IP starts in the `warming` status, and Bird's sending infrastructure warms it automatically over roughly 30 days: the volume routed through the new IP increases on a fixed schedule, and any traffic beyond what the IP can safely carry at its current stage overflows through Bird's shared pool. Your mail keeps flowing at full volume the entire time — only the split between the new IP and the shared infrastructure changes as the ramp progresses. Watch its status and progress on the [**Dedicated IPs**](https://bird.com/dashboard/w/email/ip-pools/dedicated-ips) page (**Email → IP Pools → Dedicated IPs**). ![A warming dedicated IP in the Bird dashboard with its warmup progress bar](/images/docs/dashboard-email-dedicated-ips.png) ## Monitoring warmup progress Bird syncs each warming IP's state from the sending infrastructure hourly and exposes it on the IP resource. Poll [`GET /v1/organization/dedicated-ips/:id`](/docs/api) to check where an IP is in its ramp: ```bash curl https://us1.platform.bird.com/v1/organization/dedicated-ips/dip_01krdgeqcxet5s7t44vh8rt9mg \ -H "Authorization: Bearer $BIRD_API_KEY" ``` ```json { "id": "dip_01krdgeqcxet5s7t44vh8rt9mg", "address": "192.0.2.10", "hostname": "mta123.example-infra.com", "status": "warming", "warmup_progress": 45, "warmup_started_at": "2026-05-07T14:30:00Z", "warmup_completed_at": null, "pool_id": "ipp_01krdgeqcxet5s7t44vh8rt9mg", "purchased_at": "2026-05-07T14:30:00Z" } ``` The warmup fields: - **`warmup_progress`** — completion percentage, `0`–`100`. The ramp runs through a fixed series of stages, so progress is the current stage as a fraction of the final one; expect it to advance steadily rather than jump. - **`warmup_started_at`** — when warmup began, normally the moment of purchase. - **`warmup_completed_at`** — set when warmup finishes and the IP transitions to `active`. `null` while warming. Because the state syncs hourly, a progress change can take up to an hour to appear — there is no need to poll more often than that. ## IP statuses | Status | Meaning | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `warming` | The IP is new and undergoing automatic warmup. It carries a growing share of your traffic; the rest overflows to the shared pool. | | `active` | Warmup is complete. The IP handles its full assigned volume. | | `suspended` | The IP is temporarily suspended — for example for abuse or billing reasons. It carries no traffic until reinstated. | | `pending_cancellation` | You cancelled the IP. It remains fully usable until the end of the current billing period, then it is removed. | `active` is the steady state: the ramp is done, `warmup_progress` is `100`, `warmup_completed_at` is set, and the IP carries whatever volume its pool sends with no overflow to shared infrastructure. ## Deliverability guidance The warmup schedule protects you from the volume ramp, but two decisions stay in your hands: - **The shared pool stays your default until you flip it.** Buying a dedicated IP never changes where your traffic goes: the shared pool remains your organization's default pool, so sends that don't specify a pool keep riding Bird's warmed shared infrastructure. Traffic is never forced onto a cold IP. When you're ready, make your dedicated pool the default by setting `is_default` on it — see [Dedicated IPs and pools](/docs/guides/email/dedicated-ips-and-pools). - **Wait for `active` before flipping the default.** You _can_ point traffic at a pool whose IPs are still warming — overflow routing means the mail will still be delivered — but the cleanest pattern is to leave the shared pool as default until `warmup_completed_at` is set, then switch. Never plan around dumping your full volume on a freshly purchased IP; the reputation you're building during warmup is the whole point of the exercise. If you send through multiple pools, warm each new IP the same way: purchase it into the pool, watch `warmup_progress`, and only route dedicated traffic deliberately once the IP is `active`. ## Next steps - [Dedicated IPs and pools](/docs/guides/email/dedicated-ips-and-pools) — purchasing IPs, organizing pools, and default-pool rules - [API reference: Dedicated IPs](/docs/api) — full request and response schemas --- title: "Migrate from another provider" description: "Move email sending to Bird: map the send call, re-point DNS, import suppressions, switch webhooks, and verify in the sandbox before cutover." source: https://bird.com/en-us/docs/guides/email/migrate --- # Migrate from another provider This guide walks you through moving production email to Bird. The shape of the work is the same regardless of where you're coming from: one send endpoint to re-map, four DNS records to publish, a suppression list to carry over, a webhook vocabulary to translate, and a sandbox to verify everything against before you flip traffic. The migration checklist: 1. [Map your send call](#1-map-the-send-call) to `POST /v1/email/messages` 2. [Re-point your sending domains and DNS](#2-re-point-domains-and-dns) 3. [Import your suppression list](#3-import-suppressions) 4. [Switch webhooks to Bird's event vocabulary](#4-switch-webhooks) 5. [Verify against the mail sandbox](#5-verify-in-the-sandbox-before-cutover) before cutover Steps 1, 3, and 4 depend on which provider you're leaving. Each provider guide carries the field-by-field payload mapping, where to export your suppression list, and the webhook event-name translation table: - [Migrate from SendGrid](/docs/guides/email/migrate/sendgrid) - [Migrate from Mailgun](/docs/guides/email/migrate/mailgun) - [Migrate from Amazon SES](/docs/guides/email/migrate/ses) - [Migrate from Resend](/docs/guides/email/migrate/resend) - [Migrate from Mailjet](/docs/guides/email/migrate/mailjet) - [Migrate from Mandrill](/docs/guides/email/migrate/mandrill) ## 1. Map the send call Bird has one single-send endpoint, [`POST /v1/email/messages`](/docs/api/reference/create-email-message). You build a flat JSON payload — no `personalizations` wrapper, no MIME assembly — with `from`, `to`/`cc`/`bcc` (up to 50 each), `subject`, `html` and/or `text`, optional `reply_to` (an array, 1–25 entries), and `headers` for custom email headers. A successful send returns `202 Accepted` with an `em_`-prefixed message ID; delivery outcomes arrive asynchronously through webhooks and the read endpoints. The full payload reference is in [Sending email](/docs/guides/email/sending-email); the field-by-field mapping from your current payload is in your [provider guide](#migrating-from-a-specific-provider). The distinction between `tags` and `metadata` matters more on Bird than the equivalents did at your old provider: tags are first-class filter dimensions (message list, analytics, dashboard rollups), while metadata is opaque round-trip context — stored on the message, returned on API reads, and echoed on every webhook event (alongside the `email_id`/`recipient_id` correlation IDs), so it comes back without an extra lookup. The rule of thumb and limits are in [Tags vs metadata](/docs/guides/email/sending-email#tags-vs-metadata). **Bird differences worth flagging before you port code:** - **Scheduling and templates are supported; a few fields are still reserved.** If your current integration schedules sends, Bird supports [scheduled sending](/docs/guides/email/scheduled-sending) via `scheduled_at`, and stored [templates](/docs/guides/email/templates) via `template`. A few other request fields (`contact_id`, `topic_id`, `in_reply_to_message_id`) remain reserved and return `422 unsupported_feature`; the full list is in [Sending email](/docs/guides/email/sending-email#reserved-fields). Attachments are supported too — port them to Bird's [attachments](/docs/guides/email/attachments) array. - **Suppressed recipients are rejected, visibly.** A suppressed address still gets a `recipient_id` and appears in the message's recipient list with status `rejected` and an `email.rejected` event (`rejection_reason: recipient_suppressed`) — never a silent drop. Even when every recipient is suppressed, the request is still accepted with a `202`; each recipient just comes back rejected. See [Suppressions](/docs/guides/email/suppressions). - **Set `category: "transactional"` for operational mail.** A send defaults to `marketing`, and the category controls suppression policy: `marketing` blocks on complaints and unsubscribes, `transactional` delivers through them. Newsletters and campaigns are handled correctly by the default; mark receipts, password resets, and similar operational mail `transactional` so they are not gated by an unsubscribe. ## 2. Re-point domains and DNS Register each sending domain with [`POST /v1/email/domains`](/docs/api) or on the [**Domains**](https://bird.com/dashboard/w/email/domains) page in the dashboard, then publish the records from the response's `dns_records` array — the [DNS record splitter](/tools/dns-record-splitter) can break the long DKIM value into quoted strings if your provider requires that. Bird asks for four records — and notably **not** the one most providers make you start with: | Record | Type | Gates sending | | ----------- | ----- | ------------------------------------------------------------------------------------- | | DKIM | TXT | Yes — per-organization selector and key, generated for you | | Return-path | CNAME | Yes — routes bounces to Bird and provides SPF alignment | | DMARC | TXT | Yes — any valid policy; an existing record (or one at a parent domain) already counts | | Tracking | CNAME | No — only gates branded open/click tracking | The send gate is **DKIM + return-path + DMARC** — if you don't have a DMARC record yet, the [DMARC policy generator](/tools/dmarc-policy) will write one for you. There is no SPF record to publish at your domain apex: SPF is evaluated against the envelope-from domain, which the return-path CNAME points at Bird's bounce infrastructure, so SPF passes and aligns without touching your apex. If your old provider had you add an `include:` to your apex SPF record, you can leave it in place during the transition and remove it after cutover — it neither helps nor hurts your Bird mail, and removing it frees one of the 10 DNS lookups apex SPF allows. The full explanation is in [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc#wheres-spf). Verification is automatic — Bird polls your DNS starting within seconds of registration, and most domains verify within minutes of the records propagating. Each Bird region (`us1`, `eu1`) is independent, so register the domain in every region you send from. Domain lifecycle details are in [Sending domains](/docs/guides/email/sending-domains). You can publish Bird's records while your old provider's records are still live: the DKIM record uses a Bird-specific selector, the return-path and tracking CNAMEs are new hostnames you choose, and your existing DMARC record satisfies the gate as-is — so both providers authenticate side by side until you're ready to switch traffic. ## 3. Import suppressions Carry your suppression list over **before** sending production traffic through Bird — otherwise your first sends go to addresses that already bounced or complained at your old provider, which damages the reputation you're trying to protect. Export the list from your current provider (your [provider guide](#migrating-from-a-specific-provider) has the exact endpoints), then add each address to Bird with [`POST /v1/email/suppressions`](/docs/api/reference/list-suppressions): ```bash while read -r address; do curl -s -X POST https://us1.platform.bird.com/v1/email/suppressions \ -H "Authorization: Bearer $BIRD_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"email\": \"$address\"}" done < suppressions.txt ``` Two things to know about this import path: - **There is no bulk-import endpoint yet.** The v1 suppressions API is single-entry CRUD — one address per `POST` — so importing a large list means looping, as above. Bulk import is deferred to a later release; until then, the loop is the supported path. The call is idempotent (`201` for a new record, `200` with the existing record if the address is already manually suppressed), so re-running a partial import is safe. - **Imported addresses get `reason: manual`, `applies_to: all`** — they block every category, including transactional. That is stricter than a complaint or unsubscribe record would be natively (those block only non-transactional sends), so if you need the category-aware behavior for specific addresses, see the reason taxonomy in [Suppressions](/docs/guides/email/suppressions#the-four-reasons-and-what-they-block). Going forward you don't manage bounces yourself: Bird auto-suppresses hard bounces, complaints, and unsubscribes, and fires `email_suppression.created` so your systems can mirror the list. ## 4. Switch webhooks Register an endpoint with [`POST /v1/webhooks`](/docs/api/reference/create-webhook) and subscribe it to an explicit list of event types. Bird's event names follow `resource.action` — `email.accepted` → `email.processed` → `email.delivered` on the happy path, with `email.deferred`, `email.bounced`, `email.complained`, `email.rejected`, `email.opened`, `email.clicked`, and the unsubscribe pair covering the rest. The event-name translation from your current provider's vocabulary is in your [provider guide](#migrating-from-a-specific-provider); per-event payload schemas are in the [events reference](/docs/guides/email/events). Every webhook payload carries the correlation IDs — `email_id`, `recipient_id`, and `workspace_id` — so you can tie any event back to your send; `tags` and `metadata` are **not** echoed in the payload. If your current handler relies on payload echo for correlation, the pattern ports to Bird as: store your context in `metadata` on the send, then read it back with `GET /v1/email/messages/{email_id}` when an event needs it — payload echo becomes an API lookup. Bird signs deliveries per the [Standard Webhooks](https://www.standardwebhooks.com) specification — `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers with an HMAC-SHA256 over `{id}.{timestamp}.{raw body}`. If you already verify Standard Webhooks deliveries from another platform, the exact same verification code works for Bird; otherwise the verification recipe, retry schedule, and replay tooling are in [Webhooks & events](/docs/guides/webhooks). Deliveries are at-least-once and unordered, so deduplicate on `webhook-id` and sort by the payload `timestamp` — the same discipline your current handler should already have. ## 5. Verify in the sandbox before cutover Before you move production traffic, run your full integration — the ported send call, your webhook handler, your suppression mirroring — against the [mail sandbox](/docs/guides/email/testing-sandbox). Sandbox sends go to magic addresses at `messagebird.dev` and run through the real production pipeline (same `202`, same event sequence, same signed webhook deliveries) without ever reaching an inbox or touching your reputation. A minimal pre-cutover smoke test: 1. Send to `delivered@messagebird.dev` and assert your handler processes `email.accepted` → `email.processed` → `email.delivered`. 2. Send to `bounce@messagebird.dev` and assert your bounce handling fires on `email.bounced` (simulated bounces don't write to your suppression list, so the address stays reusable). 3. Send to `suppressed@messagebird.dev` and assert you handle a lone `email.rejected` — the shape every suppressed recipient produces in production (the `rejection_reason: recipient_suppressed` detail is on the recipient record and the events API). 4. Send a `category: "marketing"` message and confirm the category shows up where you expect in events and reads. Use `+label` subaddressing (`bounce+cutover-test@messagebird.dev`) to correlate test cases — the full address appears in your events. Once the smoke test passes, cut traffic over: point your application at Bird, keep the old provider's DNS in place until your Bird domains show `capabilities.sending` verified, and watch the first hours of real deliveries in the dashboard and your webhook stream. ## Migrating from a specific provider - [SendGrid](/docs/guides/email/migrate/sendgrid) — `personalizations` → flat payload, `categories`/`custom_args` → `tags`/`metadata`, the `dropped` ↔ `email.rejected` equivalence - [Mailgun](/docs/guides/email/migrate/mailgun) — `o:*`/`v:*`/`h:*` parameters → first-class fields, bounces/complaints/unsubscribes export - [Amazon SES](/docs/guides/email/migrate/ses) — SendEmail v2 → one endpoint, configuration sets → per-message tracking flags, SNS → signed webhooks - [Resend](/docs/guides/email/migrate/resend) — near-identical payload shape, Svix-signed webhooks → Standard Webhooks ## Next steps - [Sending email](/docs/guides/email/sending-email) — the full send payload, tags vs metadata, the async 202 model - [Sending domains](/docs/guides/email/sending-domains) — registration, verification lifecycle, multi-region setup - [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc) — what each record proves, and why apex SPF isn't required - [Suppressions](/docs/guides/email/suppressions) — reasons, categories, and the management API - [Webhooks & events](/docs/guides/webhooks) — endpoint setup, Standard Webhooks verification, retries and replay - [Events](/docs/guides/email/events) — per-event payload schemas - [Testing sandbox](/docs/guides/email/testing-sandbox) — the full magic-address list and walkthroughs - [API reference](/docs/api/reference/create-email-message) — request and response schemas for the send endpoint --- title: "Migrate from Mailgun" description: "Map Mailgun's messages API parameters to Bird's send endpoint, export bounces, complaints, and unsubscribes, and translate Mailgun event names to Bird's." source: https://bird.com/en-us/docs/guides/email/migrate/mailgun --- # Migrate from Mailgun The provider-specific half of the [migration guide](/docs/guides/email/migrate): how Mailgun's `POST /v3/{domain}/messages` parameters, suppression lists, and webhook events map onto Bird. Do the steps in the main guide in order — this page is the lookup table for steps 1, 3, and 4. ## Map the send call Mailgun's form-encoded parameter prefixes (`o:` options, `v:` variables, `h:` headers) all become first-class JSON fields on [`POST /v1/email/messages`](/docs/api/reference/create-email-message): | What it does | Mailgun | Bird | | ------------------- | ---------------------------------------- | ---------------------------------------------------- | | Sender | `from` | `from` | | Recipients | `to` / `cc` / `bcc` | `to` / `cc` / `bcc` (arrays, max 50 each) | | Subject | `subject` | `subject` | | Body | `html` / `text` | `html` / `text` (at least one) | | Reply-to | `h:Reply-To` | `reply_to` (array, 1–25) | | Custom headers | `h:X-*` | `headers` (string → string object) | | Filterable labels | `o:tag` | `tags` — `{name, value}` pairs, max 20 | | Round-trip context | `v:*` / `X-Mailgun-Variables` | `metadata` — arbitrary JSON, max 2 KB | | Open/click tracking | `o:tracking-opens` / `o:tracking-clicks` | `track_opens` / `track_clicks` (default `true`) | | Category | — | `category`: `marketing` (default) or `transactional` | Porting notes: - **The request becomes JSON.** Mailgun accepts multipart form data; Bird takes a JSON body with `Content-Type: application/json` — usually the biggest mechanical change in the port. - **`v:` variables were echoed in events; map them to Bird `metadata`, which works the same way.** Bird echoes your `metadata` on every webhook event (alongside `email_id`/`recipient_id`), so your handlers get them back without an extra lookup. - **Recipient variables (`recipient-variables` batch sending)** have no direct equivalent: Bird's [batch endpoint](/docs/guides/email/sending-bulk) takes fully-rendered per-recipient entries rather than a template plus substitutions. - **`o:deliverytime` → `scheduled_at`.** Mailgun's scheduled delivery maps to Bird's [`scheduled_at`](/docs/guides/email/scheduled-sending). - **Attachments port directly.** Mailgun multipart `attachment` / `inline` files become Bird's `attachments` array with base64 `content` (set `content_id` for inline images). See [attachments](/docs/guides/email/attachments). ## Export suppressions Mailgun keeps three per-domain lists; export each and run them through the [import loop](/docs/guides/email/migrate#3-import-suppressions): - `GET /v3/{domain}/bounces` - `GET /v3/{domain}/complaints` - `GET /v3/{domain}/unsubscribes` Repeat per sending domain — Mailgun's lists are domain-scoped, while Bird suppressions are workspace-scoped, so the union of your domains' lists is what you import. ## Translate webhook events Mailgun signals temporary vs permanent failure with one `failed` event plus a `severity` field; Bird splits them: | Outcome | Mailgun | Bird | | ------------------ | -------------------- | -------------------------------------------- | | Accepted/processed | `accepted` | `email.accepted` → `email.processed` | | Delivered | `delivered` | `email.delivered` | | Temporary failure | `failed` (temporary) | `email.deferred` | | Permanent bounce | `failed` (permanent) | `email.bounced` / `email.out_of_band_bounce` | | Spam complaint | `complained` | `email.complained` | | Blocked/suppressed | — | `email.rejected` | | Open | `opened` | `email.opened` | | Click | `clicked` | `email.clicked` | | Unsubscribe | `unsubscribed` | `email.list_unsubscribed` | `email.rejected` has no Mailgun counterpart: Bird reports suppressed recipients visibly (status `rejected`, `rejection_reason: recipient_suppressed`) instead of silently skipping them — add a handler for it rather than treating it as a bounce. Verification also changes: Mailgun signs with an HMAC over `timestamp + token` inside the payload's `signature` object, while Bird signs per the [Standard Webhooks](https://www.standardwebhooks.com) specification — headers, not payload fields. Swap your verification code for the recipe in [Webhooks & events](/docs/guides/webhooks). ## Cut over Work through [domains & DNS](/docs/guides/email/migrate#2-re-point-domains-and-dns) and the [sandbox smoke test](/docs/guides/email/migrate#5-verify-in-the-sandbox-before-cutover) in the main guide — both are provider-independent. ## Next steps - [Sending domains](/docs/guides/email/sending-domains) — registration, verification lifecycle, and the DNS records you're re-pointing - [Webhooks & events](/docs/guides/webhooks) — endpoint setup and Standard Webhooks verification - [Testing sandbox](/docs/guides/email/testing-sandbox) — smoke-test the new integration before cutover - [Suppressions](/docs/guides/email/suppressions) — confirm your imported list and how Bird maintains it from here --- title: "Migrate from Mailjet" description: "Map Mailjet's Send API v3.1 message payload to Bird's flat send endpoint, carry over your suppression and blocklist, and translate Mailjet's event types to Bird's webhooks." source: https://bird.com/en-us/docs/guides/email/migrate/mailjet --- # Migrate from Mailjet The provider-specific half of the [migration guide](/docs/guides/email/migrate): how Mailjet's Send API v3.1 payload, blocklist, and Event API map onto Bird. Do the steps in the main guide in order — this page is the lookup table for steps 1, 3, and 4. The biggest structural change is the envelope. Mailjet wraps every send in a `Messages` array of PascalCase objects (`POST /v3.1/send`); Bird takes one flat, lowercase JSON object per [`POST /v1/email/messages`](/docs/api/reference/create-email-message), and many independent messages go to the [batch endpoint](/docs/guides/email/sending-bulk) instead of the `Messages` array. ## Map the send call | What it does | Mailjet (Send API v3.1) | Bird | | ------------------ | ---------------------------------------------------------------- | ----------------------------------------------------------- | | Sender | `From` — `{ "Email", "Name" }` | `from` — string or `{ "email", "name" }` | | Recipients | `To` / `Cc` / `Bcc` — `[{ "Email", "Name" }]` | `to` / `cc` / `bcc` — arrays, max 50 each | | Subject | `Subject` | `subject` | | Body | `TextPart` / `HTMLPart` | `text` / `html` (at least one) | | Reply-to | `ReplyTo` — `{ "Email", "Name" }` | `reply_to` — array, 1–25 | | Custom headers | `Headers` | `headers` — string → string object | | Round-trip context | `EventPayload` (string), echoed on events | `metadata` — arbitrary JSON, max 2 KB | | Your own send ID | `CustomID` | `Idempotency-Key` header + `tags`/`metadata` | | Attachments | `Attachments` — `{ "ContentType", "Filename", "Base64Content" }` | `attachments` — `{ "content_type", "filename", "content" }` | | Inline images | `InlinedAttachments` — with `ContentID` | `attachments` with `content_id` | | Tracking | account/template setting | `track_opens` / `track_clicks` (default `true`) | | Category | — | `category`: `marketing` (default) or `transactional` | Porting notes: - **Unwrap the `Messages` array.** A single Mailjet send is one entry in `Messages`; on Bird that's the whole request body. If you batch several entries in one `Messages` array, that maps to Bird's [batch endpoint](/docs/guides/email/sending-bulk) (up to 100 messages), not to repeated fields in one request. - **Case changes from PascalCase to lowercase.** Every field renames: `HTMLPart` → `html`, `TextPart` → `text`, `From.Email` → `from.email`. This is mechanical but touches every send. - **`EventPayload` becomes `metadata`.** Mailjet echoes a single `EventPayload` string back on every event; Bird echoes structured `metadata` (JSON) and `tags` on every webhook, so you can split correlation data into typed fields. See [tags vs metadata](/docs/guides/email/sending-email#tags-vs-metadata). - **`CustomID` splits in two.** Mailjet's `CustomID` does double duty as a dedup key and a correlation handle. On Bird, use the [`Idempotency-Key`](/docs/guides/idempotency) header for safe retries and put your own correlation ID in `metadata`. - **Templates render in your app.** Mailjet's `TemplateID` + `Variables` (server-hosted templates with `TemplateLanguage`) have no Bird equivalent yet — `template_id` is [reserved](/docs/guides/email/sending-email#reserved-fields). Render your template to HTML before sending and pass it as `html`. - **Attachments port directly** — Mailjet's `Base64Content` is Bird's base64 `content`, and `InlinedAttachments` + `ContentID` become `attachments` entries with `content_id`. See [attachments](/docs/guides/email/attachments). ## Export suppressions Mailjet keeps unreachable and unwanted addresses on its **blocklist** (hard/soft bounces and blocked sends) and tracks spam and unsubscribe signals separately. Export the blocked and bounced addresses from **Statistics → Contacts**, or pull them through the [contact management API](https://dev.mailjet.com/email/reference/contacts/), and run the list through the [import loop](/docs/guides/email/migrate#3-import-suppressions). If you send marketing mail, also carry over contacts marked unsubscribed so those preferences survive the move. ## Translate webhook events Mailjet's [Event API](https://dev.mailjet.com/email/guides/webhooks/) posts one trigger per event type. The mapping to Bird's [event vocabulary](/docs/guides/email/events): | Outcome | Mailjet | Bird | | ---------------- | --------- | ------------------------------------------------ | | Sent / accepted | `sent` | `email.accepted` → `email.processed` | | Delivered | — | `email.delivered` | | Permanent bounce | `bounce` | `email.bounced` / `email.out_of_band_bounce` | | Blocked | `blocked` | `email.rejected` | | Spam complaint | `spam` | `email.complained` | | Open | `open` | `email.opened` | | Click | `click` | `email.clicked` | | Unsubscribe | `unsub` | `email.unsubscribed` / `email.list_unsubscribed` | Two differences worth coding for: - **Bird reports delivery explicitly.** Mailjet's `sent` event means the message left Mailjet; Bird splits acceptance (`email.accepted`/`email.processed`) from the recipient mail server actually taking the message (`email.delivered`), so you get a distinct delivered signal. - **Events are recipient-scoped.** Mailjet keys events by `MessageID`; Bird's delivery events carry `recipient_id` alongside `email_id`, so a multi-recipient send produces one event stream per recipient. Bird signs deliveries per the [Standard Webhooks](https://www.standardwebhooks.com) spec — see [Webhooks & events](/docs/guides/webhooks) for verification. ## Cut over Work through [domains & DNS](/docs/guides/email/migrate#2-re-point-domains-and-dns) and the [sandbox smoke test](/docs/guides/email/migrate#5-verify-in-the-sandbox-before-cutover) in the main guide — both are provider-independent. ## Next steps - [Sending domains](/docs/guides/email/sending-domains) — registration, verification lifecycle, and the DNS records you're re-pointing - [Webhooks & events](/docs/guides/webhooks) — endpoint setup and Standard Webhooks verification - [Testing sandbox](/docs/guides/email/testing-sandbox) — smoke-test the new integration before cutover - [Suppressions](/docs/guides/email/suppressions) — confirm your imported list and how Bird maintains it from here --- title: "Migrate from Mandrill" description: "Map Mandrill's messages/send payload to Bird's send endpoint, carry over your rejection blacklist, and translate Mandrill's webhook events to Bird's." source: https://bird.com/en-us/docs/guides/email/migrate/mandrill --- # Migrate from Mandrill The provider-specific half of the [migration guide](/docs/guides/email/migrate): how Mandrill's (Mailchimp Transactional) `messages/send` payload, rejection blacklist, and webhooks map onto Bird. Do the steps in the main guide in order — this page is the lookup table for steps 1, 3, and 4. Two shape changes dominate the port. Mandrill nests everything under a `message` object and authenticates with a `key` in the request body; Bird takes a flat top-level payload and a standard `Authorization: Bearer` header. And Mandrill's recipient `type` (`to`/`cc`/`bcc` as a field on each address) becomes Bird's separate `to`/`cc`/`bcc` arrays. ## Map the send call | What it does | Mandrill (`messages/send`) | Bird | | ------------------- | ------------------------------------------------- | ----------------------------------------------------- | | Auth | `key` in request body | `Authorization: Bearer bk_...` header | | Sender | `message.from_email` / `from_name` | `from` — string or `{ "email", "name" }` | | Recipients | `message.to` — `[{ email, name, type }]` | `to` / `cc` / `bcc` — split by the `type` field | | Subject | `message.subject` | `subject` | | Body | `message.html` / `message.text` | `html` / `text` (at least one) | | Reply-to | `message.headers["Reply-To"]` | `reply_to` — array, 1–25 | | Custom headers | `message.headers` | `headers` — string → string object | | Filterable labels | `message.tags` — bare strings | `tags` — `{ name, value }` pairs, max 20 | | Round-trip context | `message.metadata` | `metadata` — arbitrary JSON, max 2 KB | | Open/click tracking | `message.track_opens` / `track_clicks` | `track_opens` / `track_clicks` (default `true`) | | Attachments | `message.attachments` — `{ type, name, content }` | `attachments` — `{ content_type, filename, content }` | | Inline images | `message.images` — `{ type, name, content }` | `attachments` with `content_id` | | Category | subaccount / tags convention | `category`: `marketing` (default) or `transactional` | Porting notes: - **Flatten and re-auth.** Drop the `message` wrapper — its fields move to the top level — and move the API key out of the body into the `Authorization` header. The `key` field has no Bird equivalent. - **Split recipients by `type`.** Mandrill marks each recipient `to`, `cc`, or `bcc` on the address object; Bird uses three separate arrays. Bucket the `to` list by its `type` field as you port. - **Tags become name/value pairs.** Mandrill tags are bare strings (`"welcome"`); Bird tags are `{ name, value }` pairs. Pick a stable `name` — `{ "name": "category", "value": "welcome" }` — so your filters and analytics group the way your Mandrill stats did. `metadata` ports across directly as JSON. - **`merge_vars` / templates render in your app.** Mandrill's handlebars-style merge tags and stored templates (`messages/send-template`) have no Bird equivalent yet — `template_id` is [reserved](/docs/guides/email/sending-email#reserved-fields). Render the final HTML in your application and send it as `html`. - **`send_at` maps to `scheduled_at`.** Mandrill's scheduled-send `send_at` maps to Bird's [`scheduled_at`](/docs/guides/email/scheduled-sending). - **Attachments port directly** — Mandrill's base64 `content` is Bird's `content`, and inline `images` (referenced as `cid:` in the HTML) become `attachments` entries with `content_id`. See [attachments](/docs/guides/email/attachments). ## Export suppressions Mandrill keeps unwanted addresses on its **rejection blacklist** — pull it with the `rejects/list` API (or export from the Rejection Blacklist view). Each entry carries a reason (`hard-bounce`, `soft-bounce`, `spam`, `unsub`, `custom`); skip the `soft-bounce` rows (transient, not a true suppression) and run the rest through the [import loop](/docs/guides/email/migrate#3-import-suppressions). Bird sorts them into its own [reasons](/docs/guides/email/suppressions) as they're imported. ## Translate webhook events Mandrill posts batched event arrays; map the `event` value to Bird's [event vocabulary](/docs/guides/email/events): | Outcome | Mandrill | Bird | | ----------------- | ------------- | ------------------------------------------------------ | | Sent / accepted | `send` | `email.accepted` → `email.processed` | | Delivered | — | `email.delivered` | | Temporary failure | `deferral` | `email.deferred` | | Permanent bounce | `hard_bounce` | `email.bounced` / `email.out_of_band_bounce` | | Soft bounce | `soft_bounce` | `email.deferred` (then `email.bounced` if it gives up) | | Spam complaint | `spam` | `email.complained` | | Unsubscribe | `unsub` | `email.unsubscribed` / `email.list_unsubscribed` | | Rejected/blocked | `reject` | `email.rejected` | | Open | `open` | `email.opened` | | Click | `click` | `email.clicked` | Two differences to code for: - **Bird reports delivery explicitly.** Mandrill's `send` event means the message was injected; Bird splits acceptance (`email.accepted`/`email.processed`) from the recipient mail server taking it (`email.delivered`). - **Events are recipient-scoped and signed differently.** Bird's delivery events carry `recipient_id` alongside `email_id` (one stream per recipient), and Bird signs per the [Standard Webhooks](https://www.standardwebhooks.com) spec rather than Mandrill's `X-Mandrill-Signature` HMAC. See [Webhooks & events](/docs/guides/webhooks) for verification. ## Cut over Work through [domains & DNS](/docs/guides/email/migrate#2-re-point-domains-and-dns) and the [sandbox smoke test](/docs/guides/email/migrate#5-verify-in-the-sandbox-before-cutover) in the main guide — both are provider-independent. ## Next steps - [Sending domains](/docs/guides/email/sending-domains) — registration, verification lifecycle, and the DNS records you're re-pointing - [Webhooks & events](/docs/guides/webhooks) — endpoint setup and Standard Webhooks verification - [Testing sandbox](/docs/guides/email/testing-sandbox) — smoke-test the new integration before cutover - [Suppressions](/docs/guides/email/suppressions) — confirm your imported list and how Bird maintains it from here --- title: "Migrate from Resend" description: "Map Resend's send payload to Bird's near-identical send endpoint, carry over suppressed addresses, and move from Svix-signed webhooks to Standard Webhooks." source: https://bird.com/en-us/docs/guides/email/migrate/resend --- # Migrate from Resend The provider-specific half of the [migration guide](/docs/guides/email/migrate): how Resend's `POST /emails` payload, suppression handling, and Svix-signed webhooks map onto Bird. Do the steps in the main guide in order — this page is the lookup table for steps 1, 3, and 4. Of the providers covered, this is the shortest port: the payloads are nearly field-for-field identical. ## Map the send call | What it does | Resend | Bird | | ------------------- | ------------------------------ | ---------------------------------------------------- | | Sender | `from` | `from` | | Recipients | `to` / `cc` / `bcc` (max 50) | `to` / `cc` / `bcc` (arrays, max 50 each) | | Subject | `subject` | `subject` | | Body | `html` / `text` | `html` / `text` (at least one) | | Reply-to | `reply_to` | `reply_to` (array, 1–25) | | Custom headers | `headers` | `headers` (string → string object) | | Filterable labels | `tags` — `{name, value}` pairs | `tags` — `{name, value}` pairs, max 20 | | Round-trip context | — (tags double as context) | `metadata` — arbitrary JSON, max 2 KB | | Open/click tracking | per-domain dashboard setting | `track_opens` / `track_clicks` (default `true`) | | Category | — | `category`: `marketing` (default) or `transactional` | Porting notes: - **Tags keep their shape, and `metadata` is an upgrade.** Resend tags are the same `{name, value}` pairs Bird uses, but they carry value constraints that pushed correlation data into tag values; on Bird, move correlation context into `metadata` (arbitrary JSON, read back via `GET /v1/email/messages/{email_id}`) and keep tags for filtering. - **Tracking moves into the payload.** Resend toggles open/click tracking per domain in the dashboard; Bird sets `track_opens`/`track_clicks` per message (both default `true`). - **`scheduled_at` maps directly.** If you schedule sends on Resend, Bird supports the same [`scheduled_at`](/docs/guides/email/scheduled-sending) field. For `react`, render your React Email templates to HTML in your application (the `render` function from `@react-email/render` works unchanged) and send the result as `html`. - **Attachments port directly.** Resend's `attachments` (base64 `content`) map to Bird's [attachments](/docs/guides/email/attachments) array; set `content_id` for inline images. - **Batch sending ports directly** — Resend's `POST /emails/batch` becomes Bird's [batch endpoint](/docs/guides/email/sending-bulk), with per-entry results in both cases. ## Export suppressions Resend doesn't expose a dedicated suppression-list export. Pull the addresses whose last event is `bounced` or `complained` — from the Emails view in the dashboard, or by walking your stored webhook events if you've been recording them — and run the list through the [import loop](/docs/guides/email/migrate#3-import-suppressions). If you use Audiences for marketing mail, also carry over contacts marked unsubscribed. ## Translate webhook events | Outcome | Resend | Bird | | ------------------ | ------------------------ | -------------------------------------------- | | Accepted/processed | `email.sent` | `email.accepted` → `email.processed` | | Delivered | `email.delivered` | `email.delivered` | | Temporary failure | `email.delivery_delayed` | `email.deferred` | | Permanent bounce | `email.bounced` | `email.bounced` / `email.out_of_band_bounce` | | Spam complaint | `email.complained` | `email.complained` | | Blocked/suppressed | `email.failed` | `email.rejected` | | Open | `email.opened` | `email.opened` | | Click | `email.clicked` | `email.clicked` | | Unsubscribe | — | `email.list_unsubscribed` | The signing scheme barely changes: Resend delivers through Svix (`svix-id`, `svix-timestamp`, `svix-signature` headers), and Bird signs per the [Standard Webhooks](https://www.standardwebhooks.com) specification that uses the same HMAC construction with `webhook-*` header names. If you verify Resend deliveries today, the same code verifies Bird's after renaming the three headers — or use the recipe in [Webhooks & events](/docs/guides/webhooks). One behavioral difference: Resend's events are message-scoped; Bird's delivery events are **recipient**-scoped (`recipient_id` alongside `email_id`), so a three-recipient send produces three delivery outcomes, not one. ## Cut over Work through [domains & DNS](/docs/guides/email/migrate#2-re-point-domains-and-dns) and the [sandbox smoke test](/docs/guides/email/migrate#5-verify-in-the-sandbox-before-cutover) in the main guide — both are provider-independent. ## Next steps - [Sending domains](/docs/guides/email/sending-domains) — registration, verification lifecycle, and the DNS records you're re-pointing - [Webhooks & events](/docs/guides/webhooks) — endpoint setup and Standard Webhooks verification - [Testing sandbox](/docs/guides/email/testing-sandbox) — smoke-test the new integration before cutover - [Suppressions](/docs/guides/email/suppressions) — confirm your imported list and how Bird maintains it from here --- title: "Migrate from SendGrid" description: "Map the SendGrid v3 Mail Send payload to Bird's send endpoint, export your suppression lists, and translate Event Webhook names to Bird's event vocabulary." source: https://bird.com/en-us/docs/guides/email/migrate/sendgrid --- # Migrate from SendGrid The provider-specific half of the [migration guide](/docs/guides/email/migrate): how SendGrid's v3 Mail Send payload, suppression lists, and Event Webhook map onto Bird. Do the steps in the main guide in order — this page is the lookup table for steps 1, 3, and 4. ## Map the send call SendGrid's `POST /v3/mail/send` wraps recipients in a `personalizations` array; Bird's [`POST /v1/email/messages`](/docs/api/reference/create-email-message) is a flat payload, so each personalization becomes its own send (or one [batch entry](/docs/guides/email/sending-bulk)). | What it does | SendGrid | Bird | | ------------------- | -------------------------------------- | ---------------------------------------------------- | | Sender | `from.email` | `from` | | Recipients | `personalizations[].to` / `cc` / `bcc` | `to` / `cc` / `bcc` (arrays, max 50 each) | | Subject | `subject` | `subject` | | Body | `content[]` (type + value) | `html` / `text` (at least one) | | Reply-to | `reply_to` / `reply_to_list` | `reply_to` (array, 1–25) | | Custom headers | `headers` | `headers` (string → string object) | | Filterable labels | `categories` | `tags` — `{name, value}` pairs, max 20 | | Round-trip context | `custom_args` | `metadata` — arbitrary JSON, max 2 KB | | Open/click tracking | `tracking_settings` | `track_opens` / `track_clicks` (default `true`) | | IP pool | `ip_pool_name` | `ip_pool` (`ipp_...` or `ipp_shared`) | | Category | — | `category`: `marketing` (default) or `transactional` | Porting notes: - **`categories` are bare strings; Bird tags are pairs.** A category like `"welcome"` becomes `{"name": "category", "value": "welcome"}` — pick a stable `name` so your dashboards filter the way your SendGrid stats did. - **`custom_args` were echoed in every event; map them to Bird `metadata`, which works the same way.** Bird echoes your `metadata` on every webhook event (alongside `email_id`/`recipient_id`), so your handlers get your context back without an extra lookup. - **Dynamic templates (`template_id`) have no Bird equivalent yet** — `template` is reserved and returns `422 unsupported_feature`. Render content in your application and send `html`/`text` until templates ship. Bird does support scheduling, though: `send_at` maps to [`scheduled_at`](/docs/guides/email/scheduled-sending). - **Attachments port directly.** SendGrid's `attachments` (base64 `content`, `type`, `filename`, `content_id` for inline) map to Bird's [attachments](/docs/guides/email/attachments) array field-for-field. - **Unsubscribe groups (`asm`)** don't port as a concept: Bird handles list-unsubscribe at the [category](/docs/guides/email/categories) level — `marketing` mail gets suppression-aware unsubscribe handling automatically. ## Export suppressions SendGrid splits suppressions across endpoints; export each and run them through the [import loop](/docs/guides/email/migrate#3-import-suppressions): - `GET /v3/suppression/bounces` - `GET /v3/suppression/spam_reports` - `GET /v3/suppression/unsubscribes` (global unsubscribes) - `GET /v3/asm/groups/{group_id}/suppressions` for each unsubscribe group you want to carry over ## Translate webhook events | Outcome | SendGrid Event Webhook | Bird | | ------------------ | ----------------------------------- | ------------------------------------------------ | | Accepted/processed | `processed` | `email.accepted` → `email.processed` | | Delivered | `delivered` | `email.delivered` | | Temporary failure | `deferred` | `email.deferred` | | Permanent bounce | `bounce` | `email.bounced` / `email.out_of_band_bounce` | | Spam complaint | `spamreport` | `email.complained` | | Blocked/suppressed | `dropped` | `email.rejected` | | Open | `open` | `email.opened` | | Click | `click` | `email.clicked` | | Unsubscribe | `unsubscribe` / `group_unsubscribe` | `email.unsubscribed` / `email.list_unsubscribed` | The `dropped` ↔ `email.rejected` equivalence is the one to test: like SendGrid, Bird reports suppressed recipients visibly (status `rejected`, `rejection_reason: recipient_suppressed`) rather than silently dropping them, so your audit logic ports cleanly. Verification changes more than the event names: SendGrid's Event Webhook signs with an ECDSA public key, while Bird signs per the [Standard Webhooks](https://www.standardwebhooks.com) HMAC scheme — swap your verification code for the recipe in [Webhooks & events](/docs/guides/webhooks). SendGrid also batches events into JSON arrays; Bird delivers one event per request. ## Cut over Work through [domains & DNS](/docs/guides/email/migrate#2-re-point-domains-and-dns) and the [sandbox smoke test](/docs/guides/email/migrate#5-verify-in-the-sandbox-before-cutover) in the main guide — both are provider-independent. ## Next steps - [Sending domains](/docs/guides/email/sending-domains) — registration, verification lifecycle, and the DNS records you're re-pointing - [Webhooks & events](/docs/guides/webhooks) — endpoint setup and Standard Webhooks verification - [Testing sandbox](/docs/guides/email/testing-sandbox) — smoke-test the new integration before cutover - [Suppressions](/docs/guides/email/suppressions) — confirm your imported list and how Bird maintains it from here --- title: "Migrate from Amazon SES" description: "Map the SES SendEmail v2 call to Bird's send endpoint, export the account-level suppression list, and replace SNS event plumbing with signed webhooks." source: https://bird.com/en-us/docs/guides/email/migrate/ses --- # Migrate from Amazon SES The provider-specific half of the [migration guide](/docs/guides/email/migrate): how the SES v2 `SendEmail` call, account-level suppression list, and SNS-based event notifications map onto Bird. Do the steps in the main guide in order — this page is the lookup table for steps 1, 3, and 4. ## Map the send call SES splits a send across `Destination`, `Content`, and configuration-set plumbing; Bird's [`POST /v1/email/messages`](/docs/api/reference/create-email-message) is one flat payload: | What it does | SES (SendEmail v2) | Bird | | ------------------- | ------------------------------- | ---------------------------------------------------- | | Sender | `FromEmailAddress` | `from` | | Recipients | `Destination.*Addresses` | `to` / `cc` / `bcc` (arrays, max 50 each) | | Subject | `Content.Simple.Subject` | `subject` | | Body | `Content.Simple.Body.Html/Text` | `html` / `text` (at least one) | | Reply-to | `ReplyToAddresses` | `reply_to` (array, 1–25) | | Custom headers | `Content.Simple.Headers` | `headers` (string → string object) | | Filterable labels | `EmailTags` | `tags` — `{name, value}` pairs, max 20 | | Round-trip context | — | `metadata` — arbitrary JSON, max 2 KB | | Open/click tracking | configuration set | `track_opens` / `track_clicks` (default `true`) | | IP pool | dedicated IP pool (config set) | `ip_pool` (`ipp_...` or `ipp_shared`) | | Category | — | `category`: `marketing` (default) or `transactional` | Porting notes: - **Configuration sets dissolve into per-message fields.** Tracking, IP pool, and event routing were configuration-set concerns on SES; on Bird the first two are payload fields and event routing is a [webhook subscription](/docs/guides/webhooks). - **Auth changes from SigV4 to a bearer token.** No request signing — `Authorization: Bearer bk_...`. Drop the AWS SDK credential chain from this code path. - **`Content.Raw` (MIME) has no equivalent** — Bird builds the message from structured fields. If you assemble raw MIME to attach files, send them as Bird's [attachments](/docs/guides/email/attachments) array instead (base64 `content` per file, `content_id` for inline images). - **SES sandbox ≠ Bird sandbox.** SES's sandbox restricts who you can send to; Bird's [mail sandbox](/docs/guides/email/testing-sandbox) is a simulator with magic addresses — no allowlisting, and nothing is delivered. ## Export suppressions Export the account-level suppression list and run it through the [import loop](/docs/guides/email/migrate#3-import-suppressions): - `GET /v2/email/suppressed-destinations` (paginate with `NextToken`; each entry carries `BOUNCE` or `COMPLAINT` as the reason) ## Translate webhook events SES publishes events through SNS or EventBridge; Bird POSTs signed webhooks directly, so the SNS topic, subscription-confirmation handshake, and message-envelope unwrapping all go away. The event names map like this: | Outcome | SES | Bird | | ------------------ | --------------- | -------------------------------------------- | | Accepted/processed | `Send` | `email.accepted` → `email.processed` | | Delivered | `Delivery` | `email.delivered` | | Temporary failure | `DeliveryDelay` | `email.deferred` | | Permanent bounce | `Bounce` | `email.bounced` / `email.out_of_band_bounce` | | Spam complaint | `Complaint` | `email.complained` | | Blocked/suppressed | — | `email.rejected` | | Open | `Open` | `email.opened` | | Click | `Click` | `email.clicked` | | Unsubscribe | `Subscription` | `email.list_unsubscribed` | `email.rejected` is new relative to SES: Bird reports suppressed recipients visibly (status `rejected`, `rejection_reason: recipient_suppressed`) rather than counting them into the send-and-bounce cycle — add a handler for it. In place of SNS message verification, Bird signs per the [Standard Webhooks](https://www.standardwebhooks.com) specification — HMAC headers on the delivery itself; the verification recipe is in [Webhooks & events](/docs/guides/webhooks). ## Cut over Work through [domains & DNS](/docs/guides/email/migrate#2-re-point-domains-and-dns) and the [sandbox smoke test](/docs/guides/email/migrate#5-verify-in-the-sandbox-before-cutover) in the main guide — both are provider-independent. One SES-specific note for the DNS step: SES's "Easy DKIM" CNAMEs stay in place during the transition — Bird's DKIM TXT record uses its own selector, so the two coexist. ## Next steps - [Sending domains](/docs/guides/email/sending-domains) — registration, verification lifecycle, and the DNS records you're re-pointing - [Webhooks & events](/docs/guides/webhooks) — endpoint setup and Standard Webhooks verification - [Testing sandbox](/docs/guides/email/testing-sandbox) — smoke-test the new integration before cutover - [Suppressions](/docs/guides/email/suppressions) — confirm your imported list and how Bird maintains it from here --- title: "Email overview" description: "What Bird Email is, how sending works, and where to find everything in this section." source: https://bird.com/en-us/docs/guides/email/overview --- # Email overview Bird Email is a complete sending platform: transactional and marketing email through one API, backed by Bird's managed delivery infrastructure. You call Bird's API with a `bk_{region}_…` key against your regional host (`https://us1.platform.bird.com` or `https://eu1.platform.bird.com`); Bird handles SMTP delivery, DKIM signing, IP reputation, and everything else between your request and the recipient's inbox. If you just want to send something, start with the [quickstart](/docs/get-started/send-your-first-email). This page is the map of everything else: the Email app in the dashboard, and the guides behind each part of it. ## The Email app in the dashboard In the [dashboard](/docs/knowledge-base/getting-started/dashboard-tour), Email is one of the workspace's channel apps. Its pages, and where each one's guide lives: | Page | What it's for | | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | [**Metrics**](https://bird.com/dashboard/w/email/metrics) | Delivery, open, bounce, and complaint rates over time — see [Tracking & metrics](/docs/guides/email/tracking-and-metrics) | | [**Emails**](https://bird.com/dashboard/w/email/emails) | Every message sent (and, on its Receiving tab, [received](/docs/guides/email/receiving-email)) — see the [email log](/docs/guides/email/email-log) | | [**Contacts**](https://bird.com/dashboard/w/email/contacts) | Stored recipients and their typed custom properties — see [Contacts](/docs/guides/email/contacts) | | [**Audiences**](https://bird.com/dashboard/w/email/audiences) | Reusable lists of contacts to target together — see [Audiences](/docs/guides/email/audiences) | | [**Domains**](https://bird.com/dashboard/w/email/domains) | Your sending domains, their DNS records, and live verification status — see [Sending domains](/docs/guides/email/sending-domains) | | [**IP Pools**](https://bird.com/dashboard/w/email/ip-pools) | Dedicated IPs and how they're grouped; only relevant at higher volumes — see [Dedicated IPs & pools](/docs/guides/email/dedicated-ips-and-pools) | | [**Suppressions**](https://bird.com/dashboard/w/email/suppressions) | Addresses Bird won't send to, and why — see [Suppressions](/docs/guides/email/suppressions) | A new workspace also shows an **Onboarding** checklist here — verify a domain, create a key, send a message, as a to-do list. It disappears from your day-to-day once you've finished it. ## How sending works You send email with `POST /v1/email/messages`. The API validates your request, checks your sending domain and suppression list, and returns `202 Accepted` with a message ID — delivery happens asynchronously from there. A `202` means Bird has durably accepted your send and is working on it; the actual delivery outcome (delivered, bounced, deferred, complained) arrives afterwards, per recipient, through [events and webhooks](/docs/guides/email/events) and the message read endpoints. Full request and response shapes live in the [API reference](/docs/api/reference/create-email-message). Two ideas shape the whole API: - **A send is not a delivery.** One message can have many recipients, and each recipient has its own lifecycle. The message-level status you see in list and read endpoints is an aggregate over recipient states. - **Why you send matters.** Every message belongs to a [category](/docs/guides/email/categories) — `transactional` or `marketing` — which determines how suppressions apply. Password resets must reach people who unsubscribed from your newsletter; categories are how Bird knows the difference. ### Reserved fields in v1 The v1 send API reserves `contact_id`, `topic_id`, and `in_reply_to_message_id` for a future release. Including any of them returns `422 unsupported_feature` today. They are named now so your integration and Bird's vocabulary converge on the same field names when they ship. (`scheduled_at` was reserved this way and has since shipped — see [scheduled sending](/docs/guides/email/scheduled-sending).) ## Sending domains and authentication Before you can send from your own address, you verify ownership of the domain via DNS. [Sending domains](/docs/guides/email/sending-domains) covers registration and verification; [DKIM, SPF, and DMARC](/docs/guides/email/dkim-spf-dmarc) explains the authentication records themselves — what each one proves to receiving mailbox providers and how Bird generates them for you. Once DMARC is in place, [BIMI](/docs/guides/email/bimi) lets you display your logo next to your messages in supporting inboxes. ## Reputation and deliverability Deliverability is mostly reputation management, and Bird handles the heavy lifting: - **[Suppressions](/docs/guides/email/suppressions)** — hard bounces, spam complaints, and unsubscribes automatically add recipients to your workspace suppression list, so you never repeatedly mail addresses that damage your reputation. You can also manage the list yourself via the API. - **[Dedicated IPs and pools](/docs/guides/email/dedicated-ips-and-pools)** — high-volume senders can isolate their reputation on their own IP addresses instead of shared infrastructure, and route different traffic through different pools. - **[IP warmup](/docs/guides/email/ip-warmup)** — new dedicated IPs need gradually increasing volume before mailbox providers trust them; this page explains the warmup process. ## Visibility Every message produces a per-recipient event timeline — accepted, processed, delivered, bounced, opened, clicked, and more: - **[Email log](/docs/guides/email/email-log)** — the per-message view in the dashboard: every send, its per-recipient timeline, and the exact content that went out. - **[Tracking and metrics](/docs/guides/email/tracking-and-metrics)** — open and click tracking (on by default, per-message switchable) and the aggregate delivery, bounce, and engagement metrics derived from your traffic. - **[Events](/docs/guides/email/events)** — the event vocabulary, recipient lifecycle, and webhook delivery to your own endpoints. ## Receiving email Bird receives as well as sends. [Receiving email](/docs/guides/email/receiving-email) covers both paths — a minted forwarding address that needs no DNS setup, or MX records on a subdomain you own — plus reading received messages and reacting to the `email.received` webhook. ## Testing without sending The [mail sandbox](/docs/guides/email/testing-sandbox) lets you exercise your whole integration without delivering to a real inbox. Sending to magic addresses at `messagebird.dev` — `delivered@`, `bounce@`, `softbounce@`, `complaint@`, `suppressed@`, `reject@`, `deferred@` — deterministically produces that outcome and drives the real webhook pipeline, so the events and payloads your handlers receive are exactly what production sends. Simulated bounces and complaints do not write to your suppression list, so the same address is reusable across test runs. ## Next steps | Page | What it covers | | --------------------------------------------------------------------- | ----------------------------------------------------------- | | [Send your first email](/docs/get-started/send-your-first-email) | Quickstart: API key to delivered message | | [Sending email](/docs/guides/email/sending-email) | The send API in depth — recipients, content, tags, metadata | | [Email log](/docs/guides/email/email-log) | Every sent message, its recipient timeline, and its content | | [Contacts](/docs/guides/email/contacts) | Stored recipients, typed properties, and bulk import | | [Audiences](/docs/guides/email/audiences) | Reusable recipient lists and how membership works | | [Sending domains](/docs/guides/email/sending-domains) | Registering and verifying the domains you send from | | [Receiving email](/docs/guides/email/receiving-email) | Forwarding addresses, MX receiving, and `email.received` | | [DKIM, SPF, and DMARC](/docs/guides/email/dkim-spf-dmarc) | Email authentication records | | [Categories](/docs/guides/email/categories) | Transactional vs marketing and suppression policy | | [Suppressions](/docs/guides/email/suppressions) | The suppression list and how it protects your reputation | | [Dedicated IPs and pools](/docs/guides/email/dedicated-ips-and-pools) | Isolating and routing your sending reputation | | [IP warmup](/docs/guides/email/ip-warmup) | Building trust on new dedicated IPs | | [Tracking and metrics](/docs/guides/email/tracking-and-metrics) | Opens, clicks, and aggregate stats | | [Events](/docs/guides/email/events) | Event types and webhooks | | [Testing sandbox](/docs/guides/email/testing-sandbox) | Simulated delivery outcomes via magic addresses | | [BIMI](/docs/guides/email/bimi) | Brand logos in the inbox | | [API reference: messages](/docs/api/reference/create-email-message) | Endpoint, schema, and error details | --- title: "Rate limits · Email" description: "Email-specific rate limits — the email_send and email_batch groups, management traffic limits, and how tiers raise your limits." source: https://bird.com/en-us/docs/guides/email/rate-limits --- # Rate limits · Email Email endpoints use the shared [rate-limit model](/docs/guides/rate-limits) — org-level limits, group-based buckets, IETF `RateLimit` headers, and `429` + `Retry-After` on exhaustion. This page covers the email-specific groups and numbers. All windows are 1 minute and fixed; only the rate varies by tier or override. ## Send limits Sending has two dedicated groups, so send quota is never consumed by management traffic (or vice versa): | Group | Endpoint | Base limit | What counts | | ------------- | --------------------------------------------------------------------- | ---------- | ------------------------------------------------------------- | | `email_send` | [`POST /v1/email/messages`](/docs/api/reference/create-email-message) | 10/min | One request = one message (to its explicit recipient list). | | `email_batch` | `POST /v1/email/batches` | 5/min | One request = up to 100 independent messages, queued at once. | The limits count **requests, not recipients** — which makes batching the lever for volume. At base limits, single sends top out at 10 messages per minute, while 5 batch calls can queue up to 500 messages in the same window. If you are sending to many recipients, move to the batch endpoint before reaching for a higher tier — see [Sending in bulk](/docs/guides/email/sending-bulk). ## Management limits Most non-send operations — listing messages, reading delivery status, managing domains, webhooks, and suppressions — use the generic management groups shared across the API: | Group | Covers | Base limit | | ------- | ------------------------------------------------------------------------------------- | ---------- | | `list` | Collection queries: list messages, message events, recipients, domains, suppressions. | 150/min | | `read` | Single-resource lookups: a message by ID, a domain, a webhook, a suppression. | 500/min | | `write` | Creates, updates, and deletes: domains, webhooks, suppressions, IP pools. | 60/min | `list` and `read` rise with your subscription tier — see [Tiers raise your limits](#tiers-raise-your-limits) below. `write` stays at its base rate for every tier. The [`RateLimit` header](/docs/guides/rate-limits#response-headers) on each response names the group that matched, so a `429` always tells you which of these buckets you exhausted: ```text RateLimit: "email_send";r=0;t=35 ``` ## Tiers raise your limits Your [subscription plan](/docs/guides/billing-and-usage) sets the send, batch, and read-heavy management ceilings. Every workspace starts on the free plan at the base rates below; higher plans raise the ceilings, and per-organization overrides can raise them further: | Tier | `email_send` | `email_batch` | `list` | `read` | | ------- | -------------- | -------------- | -------------- | -------------- | | Free | 10/min | 5/min | 150/min | 500/min | | Startup | 100/min | 25/min | 300/min | 1,000/min | | Growth | 1,000/min | 100/min | 750/min | 2,000/min | | Custom | By arrangement | By arrangement | By arrangement | By arrangement | `write` keeps its base rate (60/min) at every tier — domain, webhook, and suppression management doesn't scale with send volume. Custom-tier ceilings are set by arrangement, and if your throughput outgrows even those, per-organization overrides can raise any group further — contact support. ## Next steps - [Rate limits](/docs/guides/rate-limits) — the shared model: headers, resolution, and handling 429s - [Sending in bulk](/docs/guides/email/sending-bulk) — using batches to send to many recipients per request - [Billing & usage](/docs/guides/billing-and-usage) — subscription tiers - [Email messages API reference](/docs/api/reference/create-email-message) — send endpoint documentation --- title: "Receiving email" description: "Receive email into Bird two ways — a forwarding address with no DNS setup, or MX records on your own domain — then read messages in the dashboard, over the API, or from the email.received webhook." source: https://bird.com/en-us/docs/guides/email/receiving-email --- # Receiving email Bird can receive email as well as send it. Every message Bird receives lands in the same place regardless of how it arrived: the **Receiving** tab of the [**Emails**](https://bird.com/dashboard/w/email/inbound) page, the inbound-messages API, and the `email.received` webhook. There are two ways in, and they differ only in setup effort and what address the mail is sent to. - A **forwarding address** is a Bird-generated mailbox like `a1b2c3@inbound.eu.bird.com`. No domain, no DNS: you point an existing inbox's forwarding rule at it and Bird receives every message. Ready in under a minute. - **Receiving on your own domain** publishes MX records for a subdomain you control (say `inbound.acme.com`), after which mail sent to any address at that subdomain is delivered into Bird. Start with a forwarding address if you want to try receiving or to pipe an existing support inbox into Bird. Set up domain receiving when you want mail addressed directly to your own domain. ## Forwarding addresses Add one from the [**Forwarding**](https://bird.com/dashboard/w/email/domains/forwarding) tab of the Domains page: **Add forwarding address**, give it a name ("Support inbox"), and Bird mints the address. The address itself is fixed and never changes; the name is a label you can rename at any time. You can create as many as you need, and each one is independent, so a deleted address stops receiving without affecting the others. ![The Forwarding tab of the Domains page in the Bird dashboard: two forwarding addresses (Support inbox, Card disputes) with their generated inbound addresses, and the Add forwarding address button](/images/docs/dashboard-email-domains-forwarding.png) Programmatically, [`POST /v1/email/inbound-addresses`](/docs/api/reference/create-inbound-address) does the same thing (CLI: `bird email inbound-addresses create`). The response carries the generated `address`, its `id`, and your label. Then set that address as the forwarding target wherever the mail lives today: a Gmail or Google Workspace forwarding rule, an Outlook rule, or your helpdesk's forwarding setting. Bird receives whatever is forwarded, parses it, and stores it as an inbound message. ## Receiving on your own domain Domain receiving is a capability of a [sending domain](/docs/guides/email/sending-domains), so the domain must be registered and DKIM-verified first. Use a dedicated subdomain (`inbound.acme.com`), never your apex: receiving is enabled on the domain's own registration, and you don't want to move your root domain's mail routing onto Bird by accident. Enable it on the domain's detail page (open your domain from the [**Domains**](https://bird.com/dashboard/w/email/domains) page): the **Receiving (Optional)** card at the bottom has the toggle and lists the MX records to publish. Over the API it is one call: `PATCH /v1/email/domains/{domain_id}` with `{"inbound": {"enabled": true}}`. Enabling a domain that hasn't verified DKIM returns `422`, as does a domain that already receives inbound mail for another organization. ![The Receiving card on a domain's detail page in the Bird dashboard: the receiving toggle enabled and three verified MX records with copyable name and value fields](/images/docs/dashboard-email-domain-receive.png) Once enabled, the receiving status moves through the same lifecycle as the sending records: `pending` while Bird checks DNS for the MX records, then `verified` once they resolve to Bird. From that point, mail sent to **any** local-part at the domain — `support@`, `orders@`, anything — is delivered as an inbound message. Turning the toggle off tears receiving down and removes the MX records from the domain's record list. ## Reading what you receive The [**Receiving**](https://bird.com/dashboard/w/email/inbound) tab of the Emails page lists received messages newest first, with sender, recipient, subject, and received time. Click a row to open the message: a **Rendered** tab shows the HTML in a sandboxed frame, **Text** the plain-text body, **Details** the addressing metadata and attachments (with downloads), and **Raw** the original MIME source. The header shows three authentication indicators — the SPF, DKIM, and DMARC verdicts Bird recorded when the message arrived — so you can tell authenticated mail from spoofable mail at a glance. ![The Receiving tab of the Emails page in the Bird dashboard, listing received messages with sender, recipient, subject, and received time](/images/docs/dashboard-email-inbound.png) The same data is available over the API (CLI: `bird email inbound-messages`): - [`GET /v1/email/inbound-messages`](/docs/api/reference/list-inbound-messages) lists messages, filterable by sender, inbound address, and received time. - [`GET /v1/email/inbound-messages/{id}`](/docs/api/reference/get-inbound-message) returns one message's parsed metadata: addressing, subject, threading references, authentication results, and attachment metadata. - [`GET /v1/email/inbound-messages/{id}/body`](/docs/api/reference/get-inbound-message-body) returns the HTML and plain-text bodies. - [`GET /v1/email/inbound-messages/{id}/attachments`](/docs/api/reference/list-inbound-message-attachments) lists attachments; each can be [downloaded individually](/docs/api/reference/get-inbound-message-attachment). - [`GET /v1/email/inbound-messages/{id}/raw`](/docs/api/reference/get-inbound-message-raw) returns the original message exactly as received, in RFC 5322 (MIME) format. ## Reacting to received mail: the `email.received` webhook To process mail as it arrives — route support requests, ingest replies, trigger an agent — subscribe a [webhook endpoint](/docs/guides/webhooks) to the `email.received` event. Bird fires it after receiving and parsing each message. The payload carries enough to route and triage without a follow-up call: the `inbound_message_id`, envelope sender, recipients, subject, the `in_reply_to` reference, and the SPF, DKIM, and DMARC verdicts. When you need the body or attachments, fetch them with the inbound-messages API using the `inbound_message_id` from the event. Treat the authentication verdicts as your spam-and-spoofing filter: a message that fails DMARC claims a sender it could not authenticate as, and your handler can quarantine or drop it before any automation acts on it. ## Next steps - [Sending domains](/docs/guides/email/sending-domains) — register and verify the domain you want to receive on. - [Webhooks](/docs/guides/webhooks) — endpoints, signatures, and retries for `email.received`. - [Email log](/docs/guides/email/email-log) — the sending side of the Emails page. --- title: "Scheduled sending" description: "Schedule an email to send at a future time with the scheduled_at field — choosing the send time, viewing and canceling scheduled messages, limits, and error behavior." source: https://bird.com/en-us/docs/guides/email/scheduled-sending --- # Scheduled sending Scheduled sending lets you hand a message to Bird now and have it delivered at a future time you pick, instead of holding it in your own scheduler until the moment to call the API. You set one field on the send, Bird stores the message, and at the scheduled time it releases the message into the normal [delivery pipeline](/docs/guides/email/events) exactly as if you had called the API right then. Scheduled sends count against your plan's monthly scheduled-email allowance; scheduling past it is rejected with a `422` (`E10003`). ## Scheduling a send Add a `scheduled_at` timestamp to a normal [`POST /v1/email/messages`](/docs/api/reference/create-email-message) send. Everything else about the payload is unchanged. ```bash curl -X POST https://us1.platform.bird.com/v1/email/messages \ -H "Authorization: Bearer bk_us1_..." \ -H "Content-Type: application/json" \ -d '{ "from": "news@yourdomain.com", "to": ["subscriber@messagebird.dev"], "subject": "Your weekly digest", "html": "<p>Here is what happened this week...</p>", "category": "marketing", "scheduled_at": "2026-07-01T09:00:00Z" }' ``` The call returns `202 Accepted` with the `em_`-prefixed message ID immediately, the same as an [immediate send](/docs/guides/email/sending-email) — acceptance is synchronous, delivery is deferred. Omit `scheduled_at` (or send `null`) and the message goes out right away. On [read endpoints](/docs/api/reference/get-email-message) the message shows `status: scheduled` until its send time arrives. When the time comes, Bird releases it into the pipeline and its status advances through the usual states (`accepted` → `processed` → `delivered`, and so on). `scheduled_at` stays set on the message after it fires, so you can always see when a message was scheduled for. ## Choosing the send time `scheduled_at` is an absolute [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) timestamp. Two rules govern it: - **It must be between 30 seconds and 30 days in the future.** Earlier than 30 seconds from now, or further out than 30 days, is rejected with a `422`. The 30-second floor keeps a "schedule" from racing an immediate send; the 30-day ceiling is the maximum horizon. - **It is an exact instant, not a wall-clock time in some timezone.** Include a UTC `Z` (`2026-07-01T09:00:00Z`) or an explicit offset (`2026-07-01T09:00:00-04:00`, which is the same instant as `13:00:00Z`). Bird compares the instant against the current time; it does not interpret a bare local time or apply a recipient's timezone. To send at "9am in each recipient's local time," compute that instant yourself and schedule one send per timezone. Relative expressions like `"in 2 hours"` are **not** accepted — send a resolved timestamp. ## Viewing scheduled messages Scheduled messages that have not fired yet are served from a dedicated store, so filter the [message list](/docs/api/reference/list-email-messages) by status to see them: ```bash curl "https://us1.platform.bird.com/v1/email/messages?status=scheduled" \ -H "Authorization: Bearer bk_us1_..." ``` `status=canceled` lists messages you canceled before they sent. Once a scheduled message fires it moves into the pipeline and shows up under the delivery statuses (`accepted`, `delivered`, `bounced`, and so on), same as any other send. In the [dashboard](/docs/knowledge-base/getting-started/dashboard-tour), the email log offers the same `scheduled` and `canceled` filters. ## Canceling a scheduled send Cancel a message any time before it starts sending with [`POST /v1/email/messages/{id}/cancel`](/docs/api/reference/cancel-email-message): ```bash curl -X POST https://us1.platform.bird.com/v1/email/messages/em_019c1930687b7bfa.../cancel \ -H "Authorization: Bearer bk_us1_..." ``` A successful cancel returns `204 No Content` — there is no response body. The message's status becomes `canceled`, it never sends, and an `email.canceled` webhook fires. A few things to know: - **Only a still-scheduled message can be canceled.** If the message has already started sending, already sent, or was already canceled, the call returns `409` (`E10005`). There is a brief window as the send time arrives where a cancel can lose the race to the send itself and come back `409` — the message was already on its way. - **Canceling does not refund the scheduled-email allowance.** The unit you consumed at schedule time stays consumed. This is deliberate: it stops a schedule-then-cancel loop from working around your allowance. Your regular send allowance is untouched, because it is only charged when a message actually sends (see [Limits](#limits-and-constraints)). - **Cancel is idempotent-safe to retry** with an [`Idempotency-Key`](/docs/guides/idempotency) like any other write. There is no reschedule endpoint. To move a scheduled send to a different time, cancel it and submit a new send with the new `scheduled_at` — you get a fresh `em_` ID. ## What stays the same, and what happens at send time Scheduling changes _when_ a message is released, not how it is built or governed. - **Field and domain validation are up front.** Payload validation and the sender-domain check run when you call the API, so a malformed scheduled send fails immediately with a `422` — you find out now, not at 9am. - **The sender domain is re-checked at send time.** If your `from` domain is no longer verified when the scheduled time arrives, the message is not sent: its recipients come back `rejected` with a reason, rather than going out from an unverified domain. Keep the domain verified for the whole window between scheduling and sending. - **Suppression is evaluated at send time**, against your [suppression list](/docs/guides/email/suppressions) as it stands then — so a recipient who unsubscribes between scheduling and sending is still honored. - **[Category](/docs/guides/email/categories), [tags, and metadata](/docs/guides/email/sending-email#tags-vs-metadata) are unchanged**, and are echoed on the scheduled send's webhook events just like an immediate send. ## Limits and constraints | Constraint | Detail | | :----------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Send time | RFC 3339 instant, between 30 seconds and 30 days in the future | | Allowance | Each plan includes a monthly scheduled-email allowance (exceeding it returns a `422`), consumed at schedule time and **not** refunded on cancel; separate from your regular send allowance | | Attachments | Supported. A scheduled send may carry `attachments`, exactly like an immediate send | | Content size | The same per-message limits as an immediate send (up to the 20 MB total cap). Large bodies and attachments are stored when you schedule and restored when the send fires | | Reschedule | No reschedule endpoint — cancel and resend with a new time | ## Errors | Status | Code | When | | :----- | :------- | :---------------------------------------------------------------------------------------- | | `422` | `E10003` | You have used up your plan's monthly scheduled-email allowance | | `422` | — | `scheduled_at` is under 30 seconds or over 30 days away | | `409` | `E10005` | The message can no longer be canceled — it already started sending, sent, or was canceled | | `404` | — | No message with that ID in this workspace | ## Webhooks Two events are specific to scheduling, in addition to the usual [delivery events](/docs/guides/email/events): - **`email.scheduled`** fires when a message is accepted with a future `scheduled_at`, and carries the `scheduled_at` time. - **`email.canceled`** fires when a scheduled message is canceled before it sends. When a scheduled message fires, the normal `email.accepted` → delivery chain follows unchanged. ## Scheduling a broadcast Audience [broadcasts](/docs/guides/email/audiences) accept their own `scheduled_at`: create the broadcast with `send: true` and a future `scheduled_at`, and it dispatches to the whole audience at that time. Created without `send`, a broadcast stays an editable draft you can send later. ## Next steps - [Sending email](/docs/guides/email/sending-email) — the full send payload and the async 202 model - [Suppressions](/docs/guides/email/suppressions) — who Bird won't deliver to, and why, evaluated at send time - [Events and webhooks](/docs/guides/email/events) — the delivery pipeline a scheduled message enters when it fires - [Idempotency](/docs/guides/idempotency) — safe retries for the schedule and cancel calls - [API reference](/docs/api/reference/create-email-message) — full request and response schemas --- title: "Bulk sending: batches & broadcasts" description: "Send up to 100 independent emails in one request with POST /v1/email/batches — all-or-nothing validation, idempotent retries, and where broadcasts fit." source: https://bird.com/en-us/docs/guides/email/sending-bulk --- # Bulk sending: batches & broadcasts This guide covers reaching many recipients: the batch endpoint, `POST /v1/email/batches`, and where audience-targeted broadcasts fit today. If you only ever send one message at a time, start with [sending email](/docs/guides/email/sending-email) instead. ## When to use what - **A handful of independent messages at once** — use a batch. One request carries up to 100 complete message objects and saves you 100 round trips. - **High-volume sending from your own loop** — calling the [single-send endpoint](/docs/guides/email/sending-email) repeatedly is a perfectly good architecture; batches don't make a message cheaper or faster to deliver, they just amortize the HTTP overhead. The real volume lever is rate limits: batch requests draw from the `email_batch` rate-limit group, separate from the `email_send` group used by single sends, so batching raises how many messages you can hand over per unit of wall-clock time. - **One message to a stored audience** — that's a broadcast, covered [below](#broadcasts). Batches are not broadcasts: there is no shared content definition, no audience targeting, and no per-recipient personalization. Every message in a batch is a self-contained payload you assemble yourself. ## Batch sends `POST /v1/email/batches` takes a JSON array of 1–100 message objects. Each item is a complete, independent send request — its own `from`, `to`, `subject`, content, and optionally its own `category`, `ip_pool`, tags, and metadata. The item schema is exactly the single-send payload, so everything in [sending email](/docs/guides/email/sending-email) applies per item, including the `category` default of `marketing`. ```bash curl -X POST https://us1.platform.bird.com/v1/email/batches \ -H "Authorization: Bearer bk_us1_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: batch-2026-06-10-001" \ -d '[ { "from": "hello@yourdomain.com", "to": ["delivered@messagebird.dev"], "subject": "Your receipt", "html": "<p>Thanks for your order.</p>" }, { "from": "hello@yourdomain.com", "to": ["delivered@messagebird.dev"], "subject": "June product news", "html": "<p>What shipped this month.</p>", "category": "marketing" }, { "from": "alerts@yourdomain.com", "to": ["delivered@messagebird.dev"], "subject": "Usage threshold reached", "text": "You have used 80% of your quota." } ]' ``` ### All-or-nothing validation Every item is validated before any item is queued. If one message fails — a field-level validation error or an unverified sender domain — the entire batch is rejected with a `422` and nothing is sent. A batch never partially succeeds at accept time, so you never have to work out which half of a failed request went through; fix the offending item and resubmit the whole array. Suppression is not checked at accept time: an item whose recipients are all suppressed still accepts and gets an `em_` ID, and those recipients surface as `status: rejected` once the message is processed (see [suppressions](/docs/guides/email/suppressions)). ### The 202 response A successful batch returns `202 Accepted` with one entry per message, in submission order: ```json { "data": [ { "id": "em_019c1930687b7bfa...", "status": "accepted", "category": "transactional" }, { "id": "em_019c1930687c4e21...", "status": "accepted", "category": "marketing" }, { "id": "em_019c1930687d9b02...", "status": "accepted", "category": "transactional" } ] } ``` Each child is a regular message from this point on: track it by its `em_` ID through `GET /v1/email/messages/{id}`, its recipient and event endpoints, and webhooks — exactly as if it had been sent individually. The same [async model](/docs/guides/email/sending-email#the-async-model-what-202-means) applies: `202` means durably accepted, and per-recipient delivery outcomes arrive afterwards. ### Idempotent retries Send an `Idempotency-Key` header with the batch (as in the example above). If the request succeeded but you never saw the response, replaying it with the same key returns the original result — the same child message IDs, with an `Idempotency-Replay` header — instead of sending every message again. With up to 100 messages per request, the cost of an accidental duplicate is multiplied, so treat the key as required in production. See [idempotency](/docs/guides/idempotency). ### Attachments Each batch item can carry its own `attachments`, with the same field contract as a single send — the per-message 20 MB generated-message budget still applies. One extra limit is batch-level: the serialized JSON request body for the whole batch has a hard 20 MB cap, and base64-encoded attachments count against it, so attachment-heavy batches reach the body cap quickly. See [attachments](/docs/guides/email/attachments) for the full contract. ## Broadcasts A broadcast is one message sent to a stored audience: Bird resolves the audience's current members (minus [suppressions](/docs/guides/email/suppressions)) into the recipient set at send time, instead of you enumerating addresses. You can build the [audiences](/docs/guides/email/audiences) a broadcast targets today, from [contacts](/docs/guides/email/contacts) you store in your workspace, but broadcast sending itself is not yet available. The send and batch endpoints have no broadcast or audience fields, and the request body rejects unknown properties, so there is no way to address a broadcast for now. Until broadcast sending ships, the two options for reaching many recipients are batch sends and fanning out over the single-send endpoint from your own loop. ## Next steps - [Sending email](/docs/guides/email/sending-email) — the per-item payload in full: fields, limits, tags vs metadata - [Categories](/docs/guides/email/categories) — `marketing` vs `transactional` and what each does to suppression policy - [Idempotency](/docs/guides/idempotency) — key format, retention, and replay semantics - [API reference](/docs/api/reference/create-email-message-batch) — full batch request and response schemas --- title: "Sending domains" description: "Register a sending domain, publish its DNS records, and understand the verification lifecycle that gates live email sending." source: https://bird.com/en-us/docs/guides/email/sending-domains --- # Sending domains Before Bird will deliver email from your domain, you have to prove you own it and publish the DNS records that let mailbox providers authenticate your mail. A sending domain is the workspace-scoped resource (`dom_...`) that tracks that setup: which records to publish, what has verified, and whether the domain is ready to send. ## Sharing a domain across workspaces The same domain can be used by more than one workspace — even across different organizations — without anyone stepping on anyone else. Each organization proves ownership with its **own DKIM key**, and each workspace keeps its own return-path, tracking hostname, and tracking settings. In practice: - Two workspaces in the same org can register the same domain; the second reuses the org's existing DKIM proof, so there's nothing new to publish to authenticate. - Another organization on the same domain can never see your verification state or change your configuration. - Each region (`us1`, `eu1`) is independent: the same domain in two regions is two separate registrations with their own DNS records. Register it in each region you send from. ## Register a domain Create the domain with [`POST /v1/email/domains`](/docs/api). The call is workspace-scoped and takes the sending domain plus optional name parts for the return-path and tracking hostnames — you pass only the label (`send`, `links`), and Bird composes the full hostname under your sending domain. Omitted values default to `send` and `links`. Use a dedicated subdomain (`mail.acme.com`) rather than your registered domain, so your sending reputation stays separate from everything else on the domain. ```bash curl -s https://eu1.platform.bird.com/v1/email/domains \ -H "Authorization: Bearer $BIRD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "domain": "mail.acme.com", "return_path": { "name": "send" }, "tracking": { "name": "links" } }' ``` The response is the full domain resource: `status: pending`, the DKIM selector assigned to your org, and a `dns_records` array listing exactly what to publish. Replace `eu1` with `us1` if your workspace is in the US region (API keys are region-prefixed: `bk_eu1_...`, `bk_us1_...`). You can also register and manage domains from the [**Domains**](https://bird.com/dashboard/w/email/domains) page (**Email → Domains**) in the Bird dashboard. ![The Domains page in the Bird dashboard, showing a verified sending domain with its sending, return-path, and tracking capabilities](/images/docs/dashboard-email-domains.png) ## Publish the DNS records The `dns_records` array gives you copy-pasteable `name`, `host`, and `value` for each record. Some providers reject a long DKIM TXT value as a single string — the [DNS record splitter](/tools/dns-record-splitter) breaks it into the quoted strings those providers expect. What you publish: | Record | Type | Required for sending | What it does | | ----------------- | ----- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DKIM | TXT | Yes | Proves ownership and signs your mail with your org's key | | Return-path CNAME | CNAME | Yes | Routes bounces back to Bird and covers SPF — the SPF lookup follows the CNAME, so **no SPF record on your domain apex is needed** | | DMARC | TXT | Yes | Any valid `v=DMARC1` policy covering the sending domain — on the domain itself or on its registered (organizational) domain. A minimal `p=none` policy is enough. | | Tracking CNAME | CNAME | No | Enables branded open/click tracking hostnames; tracked links are served over HTTPS once it verifies | For what each record actually does and how to choose values, see [DKIM, SPF, and DMARC](/docs/guides/email/dkim-spf-dmarc). If you enable [receiving](/docs/guides/email/receiving-email) on the domain, its MX records join this same `dns_records` list with `purpose: inbound_mx`. For step-by-step click-paths in your DNS provider's dashboard, see the per-provider knowledge-base walkthroughs — for example [Cloudflare](/docs/knowledge-base/dns-and-domains/cloudflare), [Route 53](/docs/knowledge-base/dns-and-domains/route-53), or the [generic registrar guide](/docs/knowledge-base/dns-and-domains/generic-registrar). The dashboard detects which provider hosts your domain's nameservers (the `vendor` field on the API resource — Cloudflare, Route 53, GoDaddy, Namecheap, and others) and deep-links you straight to that provider's DNS-management page. Open your domain from the [**Domains**](https://bird.com/dashboard/w/email/domains) page to see its detail page, which shows the full set of records. ![A domain's DNS Records page in the Bird dashboard, showing the verified DKIM record with copyable name and value, and the return-path and DMARC sections below](/images/docs/dashboard-email-domain-detail.png) ## Verification lifecycle A new domain starts as `pending`. You never have to poll: Bird checks your records automatically, starting within seconds of registration and backing off over the first hours, then folding into a daily re-check that covers every active domain. Publishing your records and waiting is enough — most domains verify within minutes of DNS propagation. If you want an immediate check (for example right after editing DNS), call [`POST /v1/email/domains/{domain_id}/verify`](/docs/api). Calling it again within about a minute is a no-op rather than an error, so there's no value in hammering it — give DNS a moment to propagate between checks. The domain's top-level `status` reflects **ownership**, proven by the DKIM record: - `pending` — the DKIM record has not been published yet. - `verified` — the DKIM record is in place; ownership is confirmed. - `failed` — a DKIM record exists but does not match the expected value, or a previously verified record was removed. Correct the record to recover. - `temporary_failure` — DNS resolution failed transiently; verification is retried automatically. - `rejected` — the domain was refused for policy reasons; contact support. Readiness to send is reported separately under `capabilities`. The send gate is `capabilities.sending`, which verifies only when **DKIM, the return-path CNAME, and a DMARC policy are all in place** — SPF at the domain apex is not required. Tracking readiness (`capabilities.tracking`) is independent of the send gate: it controls whether branded open/click tracking can be used, never whether the domain may send. Verification is continuous, not one-time. The daily re-check keeps verified domains honest: if your DNS later breaks, Bird notices. To avoid flapping on transient DNS blips, a verified domain is only downgraded after **two consecutive failed re-checks** — a single bad day never knocks a domain offline, and any successful re-check resets the counter. Downgrades take effect on the next send, and a downgraded domain re-verifies automatically once the records are fixed. ## Managing domains **Regions.** Domain state is regional. If you send from both `us1` and `eu1`, register the domain in each region — each registration gets its own DKIM selector and verifies independently. **Changing return-path or tracking hostnames.** These are per-workspace choices on your assignment. A hostname that has already verified is never replaced by an unverified one: changes are staged, verified alongside your active configuration, and promoted only once the new records check out. **Open/click tracking.** The `settings` toggles are also per workspace. You can flip them on as soon as a tracking domain is configured -- enabling one without any tracking domain returns `409` -- but they only take effect on sends once that tracking domain has verified, so verification is enforced per send. One workspace's tracking choices never affect another workspace sending from the same domain. **Deletion.** `DELETE /v1/email/domains/{domain_id}` removes only your workspace's assignment. The underlying registration is reference-counted: shared infrastructure (the domain registration, your org's DKIM key, shared return-path or tracking hostnames) is cleaned up only when the last reference to it is gone, so deleting your assignment can never break another workspace or organization still using the domain. ## Next steps - [DKIM, SPF, and DMARC](/docs/guides/email/dkim-spf-dmarc) — what each record does and how to pick values. - [Per-provider DNS walkthroughs](/docs/knowledge-base/dns-and-domains/generic-registrar) — setup click-paths for Cloudflare, Route 53, GoDaddy, and more. - [Domains API reference](/docs/api) — full request/response schemas for every endpoint. --- title: "Sending email" description: "Send a single email with POST /v1/email/messages — payload fields and limits, tags vs metadata, the async 202 model, and idempotent retries." source: https://bird.com/en-us/docs/guides/email/sending-email --- # Sending email This guide covers the single-send endpoint, `POST /v1/email/messages`. You build one JSON payload with a sender, recipients, and content; Bird returns `202 Accepted` with a message ID and delivers asynchronously. Full request and response schemas live in the [API reference](/docs/api/reference/create-email-message). ## A minimal send The smallest valid payload is a `from`, at least one `to` recipient, a `subject`, and a body (`html`, `text`, or both). The `from` address must be on a domain you have verified in the workspace — with one exception for testing, below. ```bash curl -X POST https://us1.platform.bird.com/v1/email/messages \ -H "Authorization: Bearer bk_us1_..." \ -H "Content-Type: application/json" \ -d '{ "from": "hello@yourdomain.com", "to": ["delivered@messagebird.dev"], "subject": "Hello from Bird", "html": "<p>It works.</p>" }' ``` Use your regional host (`https://us1.platform.bird.com` or `https://eu1.platform.bird.com`) with a matching `bk_{region}_...` key. The recipient here is `delivered@messagebird.dev`, a [sandbox address](/docs/guides/email/testing-sandbox) that always accepts mail — handy while you wire things up, since placeholder domains like `@example.com` and anything under `.test`, `.example`, `.invalid`, or `.localhost` are rejected with a `422` (they can't receive mail, and the bounces would hurt your reputation). Haven't verified a domain yet? During onboarding you can send from the shared onboarding domain (for example `onboarding@messagebird.dev`). These sends skip the domain check but can only go to verified members of your own workspace and are subject to a daily recipient limit per organization. ## Building up the payload ### Recipients `to`, `cc`, and `bcc` each take an array of up to 50 addresses; `to` requires at least one. Each entry can be a plain email string, an RFC 5322 mailbox string (`Jane <jane@example.com>`), or an object with an optional display name. Recipients on your workspace's [suppression list](/docs/guides/email/suppressions) are blocked per the active category's policy — each one surfaces as a rejected recipient with an inspectable reason, never a silent drop. Suppression is applied asynchronously, after the `202`: a suppressed recipient is accepted and then marked `status: rejected` (with reason `recipient_suppressed`) on the read endpoints, rather than failing the request up front. Even when _every_ requested recipient is suppressed, the request is still accepted with a `202`; each recipient comes back rejected. ### Content `subject` is required (max 998 characters). Provide `html`, `text`, or both — at least one is required, and each is capped at 524,288 characters (512 KiB, roughly 512 KB for plain ASCII). Sending both is good practice: clients that can't (or won't) render HTML fall back to the text part. ### Reply-to and custom headers `reply_to` is an array (1–25 entries, same address formats as recipients). Every recipient reply goes to _all_ listed addresses, so one or two is typical. `headers` is a string-to-string object for custom email headers, e.g. `{"X-Campaign": "spring-2026"}`. ### Tracking `track_opens` and `track_clicks` both default to `true`. Set them to `false` to skip open-pixel injection or link rewriting for a given send. See [tracking and metrics](/docs/guides/email/tracking-and-metrics) for what each one does to your message. ### Category and IP pool `category` classifies the content and controls suppression policy: `marketing` blocks delivery on all suppression reasons (use it for marketing content), while `transactional` delivers through complaint and unsubscribe suppressions (use it for receipts, password resets, and similar operational messages). It defaults to `marketing`, independent of which endpoint you call; set it to `transactional` explicitly for operational mail like receipts and password resets. See [categories](/docs/guides/email/categories). `ip_pool` selects the sending pool: a pool ID (`ipp_...`) or `ipp_shared` to route through the shared pool explicitly. Omit it to use your organization's default pool. An unknown pool, or a pool with no dedicated IPs available, is rejected with a `422`. ### Field reference | Field | Type | Required | Limits / notes | | :------------- | :----------------------- | :----------- | :------------------------------------------------------------------------------ | | `from` | address | yes | Must be on a verified domain (or the onboarding domain) | | `to` | address[] | yes | 1–50 | | `cc`, `bcc` | address[] | no | Max 50 each | | `subject` | string | yes | Max 998 characters | | `html`, `text` | string | at least one | Max 524,288 characters each (512 KiB) | | `reply_to` | address[] | no | 1–25; replies hit all listed addresses | | `headers` | object (string → string) | no | Custom email headers | | `tags` | `{name, value}[]` | no | Max 20; name ≤ 32 chars, value ≤ 64 chars; `[A-Za-z0-9_-]` only | | `metadata` | object | no | Arbitrary JSON, max 2 KB serialized | | `track_opens` | boolean | no | Default `true` | | `track_clicks` | boolean | no | Default `true` | | `category` | string | no | `marketing` or `transactional`; default `marketing` | | `ip_pool` | string | no | `ipp_...` or `ipp_shared`; omit for your org's default pool | | `template` | string | no | Send a published template by ID (`emt_…`) or name; omit `subject`/`html`/`text` | | `parameters` | object | no | Template variables, max 16 KB serialized; shared across recipients | ## Sending with a template Instead of inline content, you can send a published [email template](/docs/guides/email/templates): set `template` to the template's ID (`emt_…`) or its name, omit `subject`/`html`/`text`, and pass `parameters` for its variables. ```bash curl -X POST https://us1.platform.bird.com/v1/email/messages \ -H "Authorization: Bearer bk_us1_..." \ -H "Content-Type: application/json" \ -d '{ "from": "hello@yourdomain.com", "to": ["delivered@messagebird.dev"], "template": "welcome-email", "parameters": { "first_name": "Jane" } }' ``` The template supplies the subject and body; `parameters` fill its `{{ variable }}` placeholders (a placeholder with no matching key renders empty). Everything else — recipients, `tags`, `metadata`, `category`, tracking, attachments — works exactly as an inline send. Two rules to know: - **Inline or templated, not both.** Sending `template` together with `subject`, `html`, or `text` is rejected with a `422`. - **Sends use the current published version.** A send resolves the template's live published version — drafts are never sent, and an unknown template (or one with no published version) is rejected with a `404`. See the [templates guide](/docs/guides/email/templates) for authoring, publishing, and versioning. ## Tags vs metadata Both attach your own data to a send, but they serve different jobs: - **`tags`** are structured `{name, value}` pairs (max 20 per send; name ≤ 32 characters, value ≤ 64, ASCII `[A-Za-z0-9_-]` only). They are first-class filter dimensions: filter the message list by tag and slice analytics and dashboard rollups by tag. Use tags for low-cardinality labels like `category`, `experiment_variant`, or `template_id`. - **`metadata`** is an arbitrary JSON object (max 2 KB serialized). It is stored, returned on API reads, and echoed on every webhook event — but it is not a dashboard filter dimension. Use it for round-trip context: internal IDs, foreign keys, structured context you want handed back with each event. Rule of thumb: if you want a dashboard breakdown by this dimension, use a tag; if you want context to round-trip through the API, use metadata. Both your tags and metadata are echoed on every webhook event alongside the correlation IDs (`email_id`, `recipient_id`), so you can route and reconcile against your own records without an extra lookup. You don't need to encode device, geo, mailbox provider, bounce type, or recipient domain into tags — Bird captures those as first-class analytics dimensions automatically. ```json { "tags": [{ "name": "category", "value": "onboarding" }], "metadata": { "user_id": "usr_12345", "order_id": "ord_98765" } } ``` ## The async model: what 202 means A successful send returns `202 Accepted` with an `em_`-prefixed message ID and `status: accepted`: ```json { "id": "em_019c1930687b7bfa...", "status": "accepted", "category": "transactional", "accepted_count": 1, "created_at": "2026-06-10T14:30:00Z" } ``` The `202` is returned only after the send is durably accepted — it is never accepted and then silently dropped. Hard failures you can fix (unverified sender domain, field validation) fail immediately with a `422` instead. Suppression is not one of them: suppressed recipients are accepted and later surface as `status: rejected`, never a synchronous error. Actual delivery happens asynchronously: per-recipient outcomes (delivered, bounced, deferred, complained) arrive afterwards through webhooks and the message read endpoints. Two consequences of the async design worth knowing: - **Reads return state, not content.** `GET /v1/email/messages/{id}` returns message and recipient state, never the `html` or `text` body. Bodies are stored separately: content storage is on by default per workspace (you can opt out), and when it is on the stored `html` and `text` are available for up to 30 days from the dedicated `GET /v1/email/messages/{id}/content` endpoint. - **Reads can briefly trail the 202.** There is a short window between the `202` and the message becoming visible on the read endpoints; a `404` immediately after a send resolves itself within moments. ## Attachments Add files to a send with an `attachments` array — base64-encoded file bytes inline, up to 20 per message, within a 20 MB generated-message budget. Inline images, the field contract, blocked file types, and downloading attachments back are covered in [attachments](/docs/guides/email/attachments). ## Reserved fields The following fields are part of the request vocabulary but are not yet available. Including any of them returns `422 unsupported_feature` — they are reserved now so SDKs and integrations converge on one name before launch: `topic_id`, `contact_id`, `in_reply_to_message_id` Don't build around these yet; watch the [API reference](/docs/api/reference/create-email-message) for availability. `scheduled_at` has since shipped — see [scheduled sending](/docs/guides/email/scheduled-sending). ## Retrying safely Send the `Idempotency-Key` header with a unique value per logical send, and retries become safe: if your first request succeeded but you never saw the response (timeout, dropped connection), replaying the same request with the same key returns the original result instead of sending a duplicate email. Replayed responses carry an `Idempotency-Replay` header so you can tell them apart. See [idempotency](/docs/guides/idempotency) for key format and retention. ## Next steps - [Templates](/docs/guides/email/templates) — author reusable content and send it by ID or name - [Categories](/docs/guides/email/categories) — how `marketing` vs `transactional` changes suppression behavior - [Suppressions](/docs/guides/email/suppressions) — who Bird won't deliver to, and why - [Tracking and metrics](/docs/guides/email/tracking-and-metrics) — opens, clicks, and the analytics they feed - [Testing sandbox](/docs/guides/email/testing-sandbox) — sandbox recipients and pre-verification sending - [API reference](/docs/api/reference/create-email-message) — full request and response schemas --- title: "Send email over SMTP" description: "Point any SMTP client at Bird and authenticate with an existing API key. No SDK, no code change. Covers the connection settings, authentication, and the per-key defaults that shape each send." source: https://bird.com/en-us/docs/guides/email/smtp --- # Send email over SMTP If your application, framework, or off-the-shelf software already speaks SMTP, you can send through Bird by changing three connection settings and nothing else. Rails ActionMailer, Laravel, Django, WordPress, a scan-to-email printer, an ERP: anything that can submit mail to a relay works. For teams migrating from another provider it is a credentials swap, not an integration. Mail submitted over SMTP goes through the same pipeline as the [email API](/docs/guides/email/sending-email): the same domain verification, IP pools, DKIM signing, suppression handling, tracking, events, and analytics. SMTP is a second door into one system, not a parallel one. ## What you need first - **A verified sending domain.** The address you put in `MAIL FROM` (and the message `From` header) must belong to a domain you have verified in this workspace. See [Sending domains](/docs/guides/email/sending-domains). - **An API key with the `emails` scope.** SMTP uses your normal Bird API keys. There is no separate SMTP username or password to manage. Create a key with email sending enabled from the [**API keys**](https://bird.com/dashboard/w/api-keys) page. A key without the `emails` scope cannot send, and neither can a `verify`-only key. ## Connection settings Point your client at the SMTP host for your key's region. The region is the prefix in the key itself: a `bk_eu1_…` key sends through the `eu1` host, a `bk_us1_…` key through `us1`. | Region | Host | | ------ | ------------------- | | EU | `eu1.smtp.bird.com` | | US | `us1.smtp.bird.com` | | Port | Encryption | | ---- | -------------------- | | 465 | Implicit TLS (SMTPS) | | 587 | STARTTLS | | 2525 | STARTTLS | Use whichever your client supports: - **Port 465, implicit TLS (SMTPS).** The connection is encrypted from the first byte, before any command is sent. In most libraries this is the "SSL/TLS" or "SMTPS" option. - **Ports 587 and 2525, STARTTLS.** The connection opens in plaintext and upgrades to TLS with the `STARTTLS` command before authentication. This is the "STARTTLS" (sometimes just "TLS") option. Pick 2525 if your network blocks 587. Either way the session is encrypted before your credentials are sent, so they never travel in the clear: on 587 and 2525 Bird refuses `AUTH` until `STARTTLS` has run. Port 25 is not offered for submission. ## Authenticating Authenticate with `AUTH PLAIN` or `AUTH LOGIN`. The username is the literal string `bird`, and the password is your API key: ```text Username: bird Password: bk_eu1_your_api_key ``` The username is a fixed literal — it carries no identity, the API key in the password field is what authenticates. In most SMTP tools you paste your API key into the password field and set the username to `bird`. ## What the message carries, and what the key's config carries Everything with a natural place in a MIME message comes from the message itself: the `From`, `To`, `Cc`, and `Reply-To` headers, the subject, the HTML and text bodies, and attachments and inline images. Recipients are taken from the SMTP envelope (`RCPT TO`). An address in `RCPT TO` that isn't in a visible `To` or `Cc` header is treated as a `Bcc`. A message can have at most 50 recipients across to, cc, and bcc, and the total message size is capped at 20 MB. The send options that have no place in a MIME message (which IP pool to send from, the content category, tags, and open/click tracking) come from a per-key **SMTP configuration**. A key with no configuration sends with sensible defaults: your organization's default pool, the transactional category, and tracking on. To customize them, open the [**SMTP**](https://bird.com/dashboard/w/email/smtp-configs) page and configure the key. Because the defaults live on the key, you can give each application its own key: a `wordpress-prod` key on the marketing pool with a `source=wordpress` tag, an `erp` key on a dedicated pool, each with no per-message configuration. Editing a key's configuration takes effect on new messages within a few seconds; you do not need to reconnect or restart your client. ## A full session On port 465 the client opens the TLS connection first, then runs the whole SMTP dialogue inside it: ```text ... TLS handshake ... S: 220 eu1.smtp.bird.com ESMTP Bird C: EHLO myapp S: 250-eu1.smtp.bird.com 250-SIZE 20971520 250-8BITMIME 250 AUTH PLAIN LOGIN C: AUTH PLAIN <base64 bird + key> S: 235 2.7.0 Authentication successful C: MAIL FROM:<news@acme.com> C: RCPT TO:<alice@example.com> C: DATA ... your MIME message ... C: . S: 250 2.0.0 Ok: queued as em_019c1930687b7bfa8a1b2c3d4e5f6789 ``` On port 587 or 2525 the client connects in plaintext, issues `STARTTLS` to upgrade the connection, then runs the same dialogue inside TLS. `AUTH` is not offered until the upgrade completes: ```text S: 220 eu1.smtp.bird.com ESMTP Bird C: EHLO myapp S: 250-eu1.smtp.bird.com 250-SIZE 20971520 250 STARTTLS C: STARTTLS S: 220 2.0.0 Ready to start TLS ... TLS handshake ... C: EHLO myapp S: 250-eu1.smtp.bird.com 250-SIZE 20971520 250 AUTH PLAIN LOGIN C: AUTH PLAIN <base64 bird + key> S: 235 2.7.0 Authentication successful C: MAIL FROM:<news@acme.com> C: RCPT TO:<alice@example.com> C: DATA ... your MIME message ... C: . S: 250 2.0.0 Ok: queued as em_019c1930687b7bfa8a1b2c3d4e5f6789 ``` The `250` response returns the queued message's id, the same `em_…` id you would get from the API. You can look the message up by that id in the [Email log](/docs/guides/email/email-log). ## Retrying safely The pipeline accepts a message and delivers it asynchronously, and SMTP clients retry aggressively when a connection drops. To make a retry safe, add an `X-Bird-Idempotency-Key` header to the message: a repeat within the retention window returns the id of the message that was already queued instead of sending a second copy. Use a value that is stable for the logical message, like an order id or a notification id, not a random one per attempt. ## Connection limits Each organization can hold up to 5 concurrent SMTP connections by default. Higher plans raise this limit. A connection is counted from the moment it authenticates until it closes, across every server and API key in the organization. When you are already at the limit, a further connection is refused with a transient `421` reply once it authenticates — reduce the number of simultaneous connections your senders open, or reuse a connection for several messages, and retry. This is a concurrency limit on open connections, not a cap on how many messages you can send: one connection can submit many messages in sequence. ## What comes back SMTP rejects synchronously everything the API rejects. An unverified sending domain, a recipient on a reserved domain, an unusable IP pool, a malformed message, or an exceeded quota each come back as an SMTP error at the point the problem is detected, so a misconfigured send fails fast rather than disappearing. Suppressed recipients are the one exception. As with every provider, the message is accepted and the suppressed recipient is dropped downstream, visible in the [Email log](/docs/guides/email/email-log) and [events](/docs/guides/email/events) rather than as an SMTP error. ## Next steps - [Sending domains](/docs/guides/email/sending-domains) — verify the domain you'll send from. - [Dedicated IPs & pools](/docs/guides/email/dedicated-ips-and-pools) — choose which pool a key sends from. - [Suppressions](/docs/guides/email/suppressions) — why an accepted recipient might not receive a message. - [Email log](/docs/guides/email/email-log) — find a message by the id SMTP returned. --- title: "Suppressions" description: "How the workspace suppression list protects your sender reputation, what gets suppressed automatically, and how to manage the list via the API." source: https://bird.com/en-us/docs/guides/email/suppressions --- # Suppressions Every workspace owns a suppression list: a set of email addresses Bird will not deliver to. Hard bounces, spam complaints, and unsubscribes land on it automatically, and you can add addresses yourself via the API. The list exists to protect your sender reputation — repeatedly mailing addresses that bounce or report spam is the fastest way to get your domain blocked by mailbox providers, so Bird stops those sends before they leave the platform. Suppression is a recipient-level concept. A single send can have some recipients delivered and others suppressed; suppressed recipients are visibly rejected rather than silently dropped, so you can always see exactly which addresses were blocked and why. You can browse and manage the list on the [**Suppressions**](https://bird.com/dashboard/w/email/suppressions) page (**Email → Suppressions**) in the dashboard, or use the [suppressions API](/docs/api/reference/list-suppressions) described below. ![The Suppressions page in the Bird dashboard, listing suppressed addresses with their reason and origin](/images/docs/dashboard-email-suppressions.png) ## The four reasons and what they block Each suppression record has a `reason` explaining why the address is on the list, and an `applies_to` policy controlling which [categories](/docs/guides/email/categories) it blocks: | Reason | `applies_to` | Marketing category | Transactional category | | ------------- | ------------------- | ------------------ | ---------------------- | | `hard_bounce` | `all` | Blocked | Blocked | | `complaint` | `non_transactional` | Blocked | Allowed | | `unsubscribe` | `non_transactional` | Blocked | Allowed | | `manual` | `all` | Blocked | Blocked | The split follows from what each reason means: - **`hard_bounce`** — the address does not exist. Sending is pointless in any category, so it blocks everything (`applies_to: all`). - **`complaint`** and **`unsubscribe`** — preference signals. Someone who reported your newsletter as spam may still legitimately need a password reset or an order confirmation, so these block only non-transactional sends (`applies_to: non_transactional`). Sends in the `marketing` category are blocked; sends in the `transactional` category get through. - **`manual`** — a deliberate decision by you or your team. Bird does not second-guess it: manual suppressions block every category, including transactional. ## How addresses get added automatically Bird's delivery pipeline adds suppressions in response to recipient signals — you never have to act on a bounce or complaint yourself: | Trigger | Resulting suppression | | ---------------------------------------------------- | ------------------------------------------------------ | | Hard bounce (`email.bounced`) | `reason: hard_bounce`, `applies_to: all` | | Out-of-band hard bounce (`email.out_of_band_bounce`) | `reason: hard_bounce`, `applies_to: all` | | Spam complaint (`email.complained`) | `reason: complaint`, `applies_to: non_transactional` | | Link or list unsubscribe (`email.unsubscribed`) | `reason: unsubscribe`, `applies_to: non_transactional` | Just as important is what does **not** suppress: - **Soft bounces and deferrals** (`email.deferred`) — transient failures like a full mailbox. Bird retries; the address stays sendable. - **Send-side rejections** — generation failures and policy rejections are problems with the send, not with the recipient's address. They produce `email.rejected` events but never a suppression. Auto-suppression is idempotent and first-cause-stable: if an address is already suppressed for the same scope and reason, later matching events leave the original record — including its `source_email_id`, `source_recipient_id`, and `created_at` — unchanged. Different reasons can coexist for the same address, so a hard bounce and an earlier unsubscribe each keep their own record. Auto-suppressed records carry `source_email_id` and `source_recipient_id`, linking back to the exact message and recipient event that caused the suppression — useful when a customer asks why they stopped receiving your email. An `email_suppression.created` webhook event is reserved for these automatic additions, but it is not yet delivered to webhooks — its payload is not finalized, so a subscription to it will not fire for now. Until it ships, drive suppression mirroring off the underlying lifecycle events (`email.bounced`, `email.out_of_band_bounce`, `email.complained`, `email.unsubscribed`, `email.list_unsubscribed`) on your [webhook endpoint](/docs/guides/webhooks), which carry the recipient signals that cause each suppression. ## Managing suppressions via the API The API supports single-record add, list, lookup, and delete. Email addresses are normalized to lowercase before storage and lookup, and they never appear in URL paths — addresses are PII, and keeping them out of paths keeps them out of access logs. To find a record for an address, filter the list with `?email=` instead. ### Add an address ```bash curl -X POST https://us1.platform.bird.com/v1/email/suppressions \ -H "Authorization: Bearer $BIRD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com" }' ``` Manual additions always get `reason: manual` and `applies_to: all` — they block every category. The call is idempotent: a new suppression returns `201 Created`, and if the address is already manually suppressed you get `200 OK` with the existing record instead of a conflict error. Either way the body is the suppression object: ```json { "id": "sup_01krdgeqcxet5s7t44vh8rt9mg", "email": "user@example.com", "scope": { "type": "workspace", "id": "ws_01krdgeqcxet5s7t44vh8rt9mg" }, "reason": "manual", "origin": "api_key", "applies_to": "all", "source_email_id": null, "source_recipient_id": null, "created_at": "2026-06-10T14:30:00Z" } ``` The `origin` field records how the suppression was created: `api_key` or `user` for manual additions (depending on whether the caller authenticated with an API key or a dashboard session), and `bounce_event`, `complaint_event`, or `unsubscribe_event` for automatic ones. ### List and look up ```bash curl "https://us1.platform.bird.com/v1/email/suppressions?limit=25" \ -H "Authorization: Bearer $BIRD_API_KEY" ``` The list is cursor-paginated and filterable by `reason`. To check whether a specific address is suppressed, pass it as the `email` query parameter: ```bash curl "https://us1.platform.bird.com/v1/email/suppressions?email=user@example.com" \ -H "Authorization: Bearer $BIRD_API_KEY" ``` An empty `data` array means the address is not suppressed. The same address can return multiple records when different reasons apply — for example a `hard_bounce` and an `unsubscribe`. The `email` filter matches by prefix, so a partial value such as `alice` returns every suppressed address starting with it; a complete address returns just that address. Matching is case-insensitive. ### Remove an address ```bash curl -X DELETE https://us1.platform.bird.com/v1/email/suppressions/sup_01krdgeqcxet5s7t44vh8rt9mg \ -H "Authorization: Bearer $BIRD_API_KEY" ``` Returns `204 No Content`. This is a **hard delete** — the record is removed entirely, Bird retains nothing, and the address is immediately sendable again. To delete by address, look up the ID with `?email=` first, then delete by ID. Be deliberate about removing `hard_bounce` records: if the address still doesn't exist, the next send will bounce and re-suppress it. ## What happens when you send to a suppressed address Suppressed recipients are **rejected, not silently dropped**. When a send includes a suppressed address: - The recipient still gets a `recipient_id` and appears in the message's recipient list with status `rejected`. - An `email.rejected` event is recorded for that recipient with `rejection_reason: recipient_suppressed`, visible in the [events API and your webhooks](/docs/guides/email/events). - The message itself is still accepted (`202`) and delivery proceeds for the remaining recipients. This holds even when **every** recipient of a send is suppressed: the send is still accepted (`202`) and a message ID is created, and each suppressed recipient surfaces as `rejected` with `rejection_reason: recipient_suppressed`. Suppression is evaluated during processing, not at accept time, so there is no up-front `422` for an all-suppressed send. This mirrors how other providers expose suppression (SendGrid's `dropped`, Postmark's suppressed bounces) so you can audit exactly which addresses were blocked instead of inferring it from missing deliveries. ## Testing with the sandbox The [testing sandbox](/docs/guides/email/testing-sandbox) gives you a deterministic way to exercise suppression handling: sending to `suppressed@messagebird.dev` short-circuits as if the address were on your suppression list — the recipient is rejected with `rejection_reason: recipient_suppressed` and never reaches delivery. Sandbox bounce and complaint addresses (`bounce@messagebird.dev`, `complaint@messagebird.dev`) simulate their outcomes through the real event pipeline but do **not** write to your suppression list, so the same test addresses stay reusable across runs. ## Next steps - [Categories](/docs/guides/email/categories) — `transactional` vs `marketing` and how the category interacts with suppression policy - [Unsubscribe links](/docs/guides/email/unsubscribe-links) — how opt-outs reach the suppression list with reason `unsubscribe` - [Events and webhooks](/docs/guides/email/events) — the `email.rejected` payload and the lifecycle events that drive auto-suppression - [Testing sandbox](/docs/guides/email/testing-sandbox) — magic addresses for simulating every delivery outcome - [API reference: Suppressions](/docs/api/reference/list-suppressions) — full request and response schemas --- title: "Email templates" description: "Author reusable email templates, publish immutable versions, and send them by ID or name with per-send variables." source: https://bird.com/en-us/docs/guides/email/templates --- # Email templates A template stores a reusable subject and body once, so a send only has to supply the data that changes. You author content with `{{ variable }}` placeholders, publish it as an immutable version, and then send it by a stable handle instead of inlining the markup on every call. Templates are workspace-owned; the full create, read, update, publish, and version contract lives in the [API reference](/docs/api/reference/create-email-template). ## Creating a template `POST /v1/email/templates` creates a template and its editable draft. You set the template's `name`, its content `category` (`transactional` or `marketing`), the authoring `source` format, and the draft's initial `subject`, `html`, and `text`: ```bash curl -X POST https://us1.platform.bird.com/v1/email/templates \ -H "Authorization: Bearer bk_us1_..." \ -H "Content-Type: application/json" \ -d '{ "name": "welcome-email", "description": "Welcome email", "category": "transactional", "source": "liquid", "subject": "Welcome to Acme, {{ first_name }}!", "html": "<h1>Hi {{ first_name }} 👋</h1><p>Thanks for joining.</p>" }' ``` `name` is a workspace-unique slug (lowercase letters, numbers, and hyphens) that gives the template a stable, readable handle for sending — a collision returns `409`, and you can change it later. `description` is an optional free-text label describing what the template is for. `category` and `source` are fixed at creation. **`source`** is the authoring format, and it decides how the body is rendered: - **`html`** — finished HTML, sent as-is. - **`handlebars`** — Handlebars markup, rendered at send. - **`liquid`** — Liquid markup. Today `liquid` supports **variable substitution only** (`{{ first_name }}`, `{{ contact.city }}`); filters, tags, and control flow (for example `{{ name | upcase }}` or `{% if %}`) are not yet supported and are rejected when you publish. Fuller Liquid support is coming. ## Drafts and published versions Every template has exactly one **draft** — the working copy you edit — plus any number of **published versions**, which are immutable and numbered (`1`, `2`, `3`, …). - **Editing** (`PATCH /v1/email/templates/{id}`) changes the draft in place. It's optimistic-locked: send the `revision` you last read, and a stale value returns `409` so you can reload rather than clobber a concurrent edit. - **Publishing** ([`POST /v1/email/templates/{id}/publish`](/docs/api/reference/publish-email-template)) snapshots the current draft into the next numbered version and makes it the live version. The draft stays editable for the next round. Publishing a draft with no subject or body is rejected with a `422`. The rule that matters for sending: **a send always resolves the template's current published version, and drafts are never sent.** So you can edit the draft freely while a stable version keeps going out, then publish when the change is ready. Publishing a new version changes what subsequent sends render; already-accepted sends are unaffected. List every version — the draft plus all published versions, newest first — with `GET /v1/email/templates/{id}/versions`, each carrying its number, status, and publish time. ## Personalizing with variables Placeholders in the subject and body are filled at send time from the send's `parameters` object — the same for both templated and inline sends: ```json { "template": "welcome-email", "parameters": { "first_name": "Jane" } } ``` `parameters` is shared across all recipients of the send (capped at 16 KB serialized), and a placeholder with no matching key renders empty rather than erroring. Per-recipient personalization is not yet available. ## Sending with a template Set the send's `template` field to the template's ID (`emt_…`) or its name and omit `subject`/`html`/`text` — the template supplies them. Both resolve the template's current published version; the name is a readable handle you can change, while the ID is the template's permanent identity. An unknown template, or one with no published version, is rejected with a `404`. The full send-side contract — including that `template` and inline content are mutually exclusive — is in [sending with a template](/docs/guides/email/sending-email#sending-with-a-template). ## Next steps - [Sending email](/docs/guides/email/sending-email) — the full send payload, and how templated sends fit it - [Categories](/docs/guides/email/categories) — how a template's `category` drives suppression policy - [API reference](/docs/api/reference/create-email-template) — the full template create, publish, and version contract --- title: "Test email delivery (mail sandbox)" description: "Trigger deterministic delivery outcomes (bounces, complaints, suppressions, rejections) through the real event and webhook pipeline, without a real inbox." source: https://bird.com/en-us/docs/guides/email/testing-sandbox --- # Test email delivery (mail sandbox) The mail sandbox lets you exercise your entire email integration — webhook handlers, suppression logic, retry behaviour — with real API calls that never deliver anywhere. Send to a magic address at `messagebird.dev` and the outcome is determined by the address itself: `bounce@messagebird.dev` always bounces, `delivered@messagebird.dev` always delivers, and so on. The full address list is also mirrored at [messagebird.dev](https://messagebird.dev). What makes this different from validation-only sandbox modes: a sandbox send runs through the **real production pipeline**. You get the same `202` response, the same per-recipient [event](/docs/guides/email/events) sequence, and the same [webhook](/docs/guides/webhooks) deliveries — identical event types and payload shapes, with no "this is a test" flag — as a production send. The message is simply never handed to the delivery infrastructure, so it never reaches a real inbox. That means zero reputation risk: simulated bounces and complaints don't touch your sending reputation, and they don't write to your [suppression list](/docs/guides/email/suppressions), so the same bounce address is reusable across every test run. There's nothing to enable and no special API key. Sandbox behaviour is triggered purely by the recipient address on the normal send endpoints (`POST /v1/email/messages` and `POST /v1/email/batches`). ## Magic addresses All addresses are on `@messagebird.dev`. The local-part selects the outcome: | Address | Simulated outcome | Events you'll receive | Notes | | ------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `delivered@` | Accepted by the receiving ISP and delivered | `email.accepted` → `email.processed` → `email.delivered` | The happy path | | `bounce@` / `hardbounce@` | Hard bounce — SMTP `550 5.1.1 Unknown User`, bounce class 10 | `email.accepted` → `email.processed` → `email.bounced` | Does **not** add the address to your suppression list, so it's reusable | | `softbounce@` | Transient soft bounce — SMTP `451`, bounce class 20 | `email.accepted` → `email.processed` → `email.bounced` (transient) | No suppression write | | `deferred@` / `delay@` | Transient deferral — SMTP `451`, bounce class 21 | `email.accepted` → `email.processed` → `email.deferred` | | | `complaint@` / `spam@` | Recipient marks the message as spam (feedback-loop complaint) | `email.accepted` → `email.processed` → `email.complained` | No suppression write; reusable | | `suppressed@` | Recipient is already on the suppression list | `email.accepted` → `email.rejected` with `rejection_reason: recipient_suppressed` | Short-circuits at processing exactly like a real suppressed recipient; no send is attempted, so no `email.processed` or delivery events follow | | `reject@` | The delivery provider rejects the transmission (4xx) | `email.accepted` → `email.rejected` with `rejection_reason: transmission_failed` | No delivery events follow the rejection | A message can mix sandbox and real recipients — each recipient gets its own lifecycle, real recipients are delivered normally, and sandbox recipients are simulated. ## Addressing rules - **Detection is by local-part only**, and only on the `messagebird.dev` domain. `bounce@yourdomain.com` is just a normal address. - **Case-insensitive**: `Bounce@messagebird.dev` and `bounce@messagebird.dev` behave identically. - **`+label` subaddressing is stripped before matching**: `bounce+signup-flow@messagebird.dev` still bounces. Use labels to correlate test cases — the full address (label included) appears in your events and webhooks, so each test run can tag its own recipients. ## Walkthrough: simulate a bounce, end to end You don't need a verified sending domain — send from Bird's shared onboarding domain, `onboarding@messagebird.dev`, the same one used in [Send your first email](/docs/get-started/send-your-first-email). Sandbox recipients are exempt from the onboarding domain's usual recipient restrictions (they do still count toward its daily send quota), so the whole test is self-contained on `messagebird.dev`: one Bird-owned domain on both ends. Make sure you have a webhook endpoint subscribed to email events (see [Webhooks](/docs/guides/webhooks)), then send: ```bash curl -X POST https://us1.platform.bird.com/v1/email/messages \ -H "Authorization: Bearer $BIRD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "onboarding@messagebird.dev", "to": ["bounce+signup-flow@messagebird.dev"], "subject": "Sandbox bounce test", "html": "<p>This message will hard-bounce.</p>" }' ``` If your key starts with `bk_eu1_`, call `https://eu1.platform.bird.com` instead. The API responds `202 Accepted` with an `em_*` message ID — indistinguishable from a production send, which is the point: the code path you're testing is your real one. Your webhook endpoint then receives `email.accepted`, `email.processed`, and finally `email.bounced`. The `email.bounced` `data` carries the full bounce classification — `bounce_type`, `bounce_class`, `bounce_code`, `bounce_description`, and `sending_ip` — alongside the `tags` and `metadata` echoed on every event (null when the send carried none). For a hard bounce the simulator returns SMTP `550 5.1.1 Unknown User` with bounce class 10, so the payload looks like this (the authoritative field reference is the [events page](/docs/guides/email/events)): ```json { "type": "email.bounced", "timestamp": "2026-06-10T14:03:12Z", "data": { "email_id": "em_019c1930687b7bfa8a1b2c3d4e5f6789", "recipient_id": "er_01krdgeqcxet5s7t44vh8rt9mg", "workspace_id": "ws_01krdgeqcxet5s7t44vh8rt9mg", "recipient": "bounce+signup-flow@messagebird.dev", "recipient_role": "to", "bounce_type": "hard", "bounce_class": 10, "bounce_code": "550", "bounce_description": "5.1.1 Unknown User", "sending_ip": null, "tags": null, "metadata": null } } ``` Note there is no simulator flag anywhere in the payload — the event type and shape are exactly what a real hard bounce produces. The only thing that identifies it as simulated is the recipient address itself, so if your handler needs to special-case test traffic, key off the `messagebird.dev` recipient domain. To verify the other half of bounce handling — that an already-suppressed recipient never gets sent to — repeat the send with `suppressed@messagebird.dev` and assert you receive `email.accepted` followed by `email.rejected` with `rejection_reason: recipient_suppressed`, and no `email.processed` or delivery events. This mirrors a real suppressed recipient exactly: the message is accepted, then short-circuited at processing before any send. ## What the sandbox does and doesn't do - **No suppression-list writes.** Simulated hard bounces and complaints do **not** add the recipient to your [suppression list](/docs/guides/email/suppressions) — that's what keeps the addresses reusable. Your webhook still fires (`email.bounced`, `email.complained`), so your own suppression logic is fully exercised; to test the already-suppressed rejection path, use the dedicated `suppressed@` address. - **No real delivery, ever.** Sandbox recipients are intercepted before the message is handed to the delivery infrastructure. Nothing is transmitted, no inbox is involved, and your sending reputation is untouched. - **No content validation.** Because the message is never assembled for delivery, the sandbox exercises the event and webhook path, not rendering or content checks. - **Opens and clicks aren't simulated yet.** No recipient address can trigger an engagement event, so `email.opened` / `email.clicked` simulation is planned for a later phase. Today the sandbox covers delivery-pipeline outcomes only. - **Stats currently include sandbox traffic.** Sandbox sends count toward your workspace's aggregate stats and bounce/complaint rates for now; an exclusion filter is planned. Heavy sandbox bouncing will skew your dashboards (not your reputation). ## Next steps - [Events](/docs/guides/email/events) — the full event vocabulary and per-recipient lifecycle - [Webhooks](/docs/guides/webhooks) — subscribing, signature verification, and retries - [Send your first email](/docs/get-started/send-your-first-email) — the onboarding-domain quickstart this walkthrough builds on - [Suppressions](/docs/guides/email/suppressions) — how the real suppression list works --- title: "Tracking & metrics" description: "How open and click tracking work per send, and how to read your aggregate email metrics in the Bird dashboard." source: https://bird.com/en-us/docs/guides/email/tracking-and-metrics --- # Tracking & metrics The **Metrics** page in the Bird dashboard is where you see how your email is doing: how much of it reached the inbox, how recipients engaged, and whether your bounce or complaint rates are drifting toward trouble. This guide starts with that page — what each number means and when to act on it — then explains the open- and click-tracking instrumentation that produces the engagement numbers behind it. Metrics are the aggregate view across everything your workspace sends. For the per-recipient outcome of a single message — did _this_ address bounce, open, or click — see [events and webhooks](/docs/guides/email/events). ## Read your metrics The [**Metrics**](https://bird.com/dashboard/w/email/metrics) page lives at **Email → Metrics** in the Bird dashboard. Every number respects the range picker in the top right (last 24 hours, 7, 30, or 90 days) and updates as new events arrive, so a report you open now reflects sends and engagement up to the last few minutes. ![The Metrics page in the Bird dashboard: the delivery, open, bounce, and complaint rate cards above the delivery and engagement over time chart](/images/docs/dashboard-email-metrics.png) Rates are computed against **event time, not send time** — engagement earned today for a message sent last week lands in today's numbers, and a bounce that arrives hours after the send counts when it arrives. ### The four health cards The row of cards at the top is your at-a-glance health check. Each shows a rate for the selected range, the count behind it, and a **Healthy** or **Risk** label so you can tell at a glance whether anything needs attention. - **Delivery rate** — the share of accepted messages that reached the recipient's mail server. Bird marks it Healthy above 95%; below that, something is rejecting your mail (bad addresses, authentication gaps, or reputation problems) and the bounce breakdown below tells you which. - **Open rate** — unique opens as a share of delivered mail, counting only opens Bird can attribute to a person (see [prefetched opens](#open-tracking) below). Healthy above 35%. Treat opens as a soft signal: a missing open doesn't mean the message went unread (text-only clients, blocked images), so prefer clicks for decisions that matter. - **Bounce rate** — the share of messages that failed delivery. The card shows your rate against a **5% limit**; at or above it the card flips to Risk. Sustained high bounces mean list-hygiene problems and put your sending reputation at risk. - **Complaint rate** — the share of delivered mail that recipients marked as spam. The limit here is **0.1%** — small, because mailbox providers act on complaints fast. This is the number to watch most closely; crossing it can get your mail throttled or blocked at the provider. The 95% / 35% / 5% / 0.1% thresholds are the guardrails Bird uses to color the cards. They're deliberately conservative — a card can read Healthy and still have room to improve. ### Delivery & engagement over time The main chart plots **sent**, **opened**, and **bounced** volume across the range so you can spot trends and one-off spikes — a bad send, a list import gone wrong, a campaign that landed well. The bucket size follows the range (hourly for 24 hours, daily for longer windows). ### Bounce rate and its causes Below the chart, the bounce card breaks your bounces down by cause, so a rising bounce rate points you at the fix: - **Hard** — permanent failures (invalid address, non-existent domain). These auto-[suppress](/docs/guides/email/suppressions) the address. - **Soft** — transient failures (mailbox full, server temporarily unavailable). Bird retries these. - **Block** — the receiving server blocked your sending IP for reputation reasons. - **Admin** — administrative refusals (relaying denied, a blocklisted domain). - **Undetermined** — the server's response was too ambiguous to classify. - **Out-of-band** — a late bounce that arrived _after_ the message was already accepted; it doesn't change the recipient's delivered status but still counts against your sender health. These map to the same `bounce_type` values on the event stream — see the [bounce classification table](/docs/guides/email/events#bounce-classification) for the underlying codes. ### Complaint rate over time The complaint card charts your spam-complaint rate across the range. Because the healthy ceiling is so low, watch the _shape_ here as much as the number: a steady climb, even below the limit, is an early warning that a segment or campaign is annoying recipients. ### Delivery latency The latency table shows how long messages take to move through the pipeline, split into **Processing** (Bird accepting and queueing the message), **Delivery** (time at the receiving server), and **Total**, each at the p50, p95, and p99 percentiles. Use p95/p99 to catch the slow tail — a healthy median with a slow p99 usually points at one provider deferring your mail. ### Breakdowns The **Breakdowns** panel slices the same delivery and engagement numbers three ways, so you can compare senders side by side and isolate a problem to its source: - **By domain** — each sending domain's sent, delivered, bounce, open, complaint, and health status. - **By IP** — the same per sending IP, with the pool each IP belongs to. - **By tag** — the [tags](/docs/guides/email/sending-email) you set at send time. This is the most flexible cut: tag a campaign, template, or experiment variant and compare them directly. Each tab ranks the top senders for the range and supports a search filter; when a dimension has more distinct values than fit, the panel notes "Top N of M". ![The lower half of the Metrics page: the delivery-latency table (processing, delivery, and total at p50/p95/p99) above the Breakdowns panel, with delivery and engagement broken down by sending domain, IP, and tag](/images/docs/dashboard-email-metrics-breakdowns.png) ### A note on sandbox traffic Sends to [sandbox addresses](/docs/guides/email/testing-sandbox) on `messagebird.dev` **count toward these metrics** — a heavy test run moves your dashboard numbers and bounce/complaint rates. Because sandbox sends never leave Bird, they never affect your real sending reputation, but keep them in mind when reading a range that includes testing. ## How tracking works Open and click tracking are what turn a delivered message into the engagement numbers above. You enable them per sending domain on the [**Domains**](https://bird.com/dashboard/w/email/domains) page (**Email → Domains**), where you also add the tracking CNAME. Both are opt-in and instrument each message as it's sent. ### Open tracking When open tracking is enabled, Bird injects a transparent tracking pixel into the HTML part of your message. When a recipient's mail client fetches the pixel, Bird records an `email.opened` event. Modern inboxes complicate this signal: Apple Mail Privacy Protection and Gmail's image proxy auto-fetch images on the recipient's behalf, producing opens that no human triggered. Bird flags these as prefetched (`is_prefetched: true`) on the open event and counts raw and non-prefetched opens separately. Your open rate is based on **unique non-prefetched opens over delivered**, so privacy-proxy noise doesn't inflate it — and a recorded open may still be a proxy prefetch, which is why opens are a soft signal. ### Click tracking When click tracking is enabled, Bird rewrites the links in your HTML through a tracking redirect: the recipient clicks the rewritten URL, Bird records an `email.clicked` event (including the original `url`), and immediately redirects to the destination. Rewritten links are served from your domain's **branded tracking hostname** — the tracking CNAME you configure on your [sending domain](/docs/guides/email/sending-domains) (by default `links.` under your sending domain). Click and open tracking only run once that CNAME has verified; until then nothing is rewritten or pixelled. Once it verifies, tracked links are served over HTTPS from your own branded hostname. Tracking readiness is reported separately from sending readiness (`capabilities.tracking` vs `capabilities.sending` on the domain resource) — an unverified tracking CNAME never blocks sending, only tracking. ### Per-send toggles `track_opens` and `track_clicks` are per-send booleans on [`POST /v1/email/messages`](/docs/guides/email/sending-email), and both default to `true`. Set either to `false` to skip pixel injection or link rewriting for that send — common for transactional mail where rewritten links are undesirable (password resets, security notifications). Effective tracking is the **AND** of the per-send flag and your domain's tracking settings: a message is tracked only when the per-send flag is `true`, your domain's open or click tracking setting is enabled, and the tracking CNAME has verified. Those domain-level settings default to off, so even though `track_opens` and `track_clicks` default to `true`, opens and clicks aren't tracked until you set up and verify a branded tracking domain and turn the setting on — a per-send `true` can't enable tracking on its own. A per-send `false` always suppresses tracking for that message. The message resource echoes the resolved values back on reads. ## Advanced analytics The Metrics page covers your own sending. Two Bird products go further, beyond what you can see from your own event stream: - **[Inbox Tracker](https://bird.com/en-us/products/email/inbox-tracker)** — inbox placement monitoring: where your mail actually lands (inbox, spam, or missing) across mailbox providers, plus authentication and reputation signals. - **[Competitive Tracker](https://bird.com/en-us/products/email/competitive-tracker)** — benchmark your email program against competitors: their send volume, campaign cadence, and subject lines. Both are enterprise add-ons — [contact sales](https://bird.com/en-us/demo) to discuss access. ## Next steps - [Events and webhooks](/docs/guides/email/events) — the per-recipient event stream behind the aggregate view - [Sending domains](/docs/guides/email/sending-domains) — setting up the tracking CNAME for branded links - [Sending email](/docs/guides/email/sending-email) — `track_opens`, `track_clicks`, and tags on the send payload --- title: "Tracking domain" description: "The branded CNAME that serves your open and click tracking — how to customize it, how it verifies, and how it relates to your per-send tracking flags." source: https://bird.com/en-us/docs/guides/email/tracking-domain --- # Tracking domain A **tracking domain** is a branded hostname on your [sending domain](/docs/guides/email/sending-domains) that serves open and click tracking. With it in place, the links Bird rewrites for [click tracking](/docs/guides/email/tracking-and-metrics) point at your own hostname over HTTPS instead of a generic Bird domain — better-looking to recipients, and it keeps link reputation tied to your brand. Unlike the [bounce domain](/docs/guides/email/bounce-domain), it's optional: it gates branded tracking, never sending. ## The record The tracking domain is a CNAME under your sending domain that resolves to Bird's tracking infrastructure in your region: | Type | Host | Value | | ----- | ------------------- | ------------------------- | | CNAME | `links.example.com` | `<region>.links.bird.com` | The value is region-specific (`us1.links.bird.com` or `eu1.links.bird.com`) — copy it from your domain's detail page (open it from the [**Domains**](https://bird.com/dashboard/w/email/domains) page) or the domain resource. The host defaults to `links.` under your sending domain. ## Customizing the hostname Choose the label when you register the domain by passing `tracking.name`; Bird composes the full hostname: ```bash curl -s https://us1.platform.bird.com/v1/email/domains \ -H "Authorization: Bearer $BIRD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "domain": "mail.acme.com", "tracking": { "name": "click" } }' ``` That registers `click.mail.acme.com` as the tracking host instead of the default `links.mail.acme.com`. Like the return-path, it's a per-workspace choice. Sending a `tracking: null` on a `PATCH` removes the tracking domain — sending is unaffected, only branded tracking stops. ## It doesn't gate sending A domain with verified DKIM, return-path, and DMARC can send even with no tracking record at all — `capabilities.tracking` is reported separately from `capabilities.sending`. Until the tracking CNAME verifies, opens and clicks simply aren't tracked through your branded host; nothing about delivery changes. Tracking is the **AND** of three things, so a verified tracking domain alone doesn't turn tracking on: 1. The per-send flag — `track_opens` / `track_clicks` on the message (both default `true`). 2. The domain setting — `settings.open_tracking` / `settings.click_tracking` (both default **off**; enabling one with no tracking domain configured returns `409`). 3. A verified tracking CNAME. So opens and clicks start flowing only once you've configured and verified a tracking domain _and_ flipped the domain setting on. See [tracking & metrics](/docs/guides/email/tracking-and-metrics) for what each flag does to a message. ## Changing it later Tracking-hostname changes are staged the same way return-path changes are: a hostname that's already verified is never replaced by an unverified one. Bird verifies the new tracking CNAME alongside the active one and promotes it only once the new record checks out, so branded tracking never breaks mid-change. A tracking hostname is claimed by one organization per region — another org can't register the same tracking host while you hold it. If you stop using a hostname and want to free it for reuse, release it from your workspace once your replacement has verified. ## Next steps - [Tracking & metrics](/docs/guides/email/tracking-and-metrics) — what open and click tracking record, and the per-send flags - [Sending domains](/docs/guides/email/sending-domains) — registering a domain and the verification lifecycle - [Bounce domain](/docs/guides/email/bounce-domain) — the required return-path hostname - [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc) — every record on the domain and what it proves --- title: "Unsubscribe links" description: "Give recipients a way to opt out — the one-click List-Unsubscribe header, an in-body unsubscribe link, the webhook events Bird fires, and automatic suppression." source: https://bird.com/en-us/docs/guides/email/unsubscribe-links --- # Unsubscribe links Marketing mail needs a way for recipients to opt out, and bulk senders are now required by Gmail and Yahoo to offer [one-click unsubscribe](/docs/knowledge-base/deliverability/gmail-yahoo-requirements). There are two paths a recipient can take — the unsubscribe button the mailbox provider renders from your `List-Unsubscribe` header, and a link you put in the message body — and Bird reports both as webhook events and acts on them by [suppressing](/docs/guides/email/suppressions) the address. This guide covers wiring up both, the events Bird fires, and how suppression closes the loop. ## Set the category first Unsubscribes only mean something for mail a recipient can opt out of, so the first thing to get right is the [category](/docs/guides/email/categories). A send is `category: "marketing"` by default, so marketing, newsletter, and announcement mail is treated correctly with no extra work: a recipient who opts out is suppressed with reason `unsubscribe`, which blocks `marketing` sends but still lets `transactional` mail (password resets, receipts) through. Set `category: "transactional"` explicitly for operational mail, so it is never gated by an unsubscribe and carries no opt-out affordance. ## One-click List-Unsubscribe (RFC 8058) The one-click mechanism is the unsubscribe button mailbox providers render in their own UI, driven by two headers on your message: ```text List-Unsubscribe: <https://yourapp.com/unsubscribe?token=abc123>, <mailto:unsubscribe@yourdomain.com?subject=unsubscribe> List-Unsubscribe-Post: List-Unsubscribe=One-Click ``` `List-Unsubscribe` lists where to send the opt-out — an HTTPS URL, a `mailto:`, or both. `List-Unsubscribe-Post: List-Unsubscribe=One-Click` is what makes it _one-click_ (RFC 8058): the provider sends a plain `POST` to your HTTPS URL when the recipient taps unsubscribe, with no confirmation page. Your endpoint must accept that unauthenticated `POST` and record the opt-out without requiring the recipient to log in or confirm — that is the contract Gmail and Yahoo check for. On a `marketing` send Bird sets these headers for you, so the compliant one-click button appears with no work on your side. This is true however you send: a broadcast from the dashboard, or a `marketing` message through the API (`POST /v1/email/messages`), which is now the default category. Because Bird owns the header on marketing mail, supplying your own `List-Unsubscribe` or `List-Unsubscribe-Post` on a `marketing` send is rejected with a `422`. On a `transactional` send Bird adds no unsubscribe header, and you may set your own through the [`headers`](/docs/guides/email/sending-email#reply-to-and-custom-headers) field if you have a reason to. When a recipient uses the provider's button, Bird fires an [`email.list_unsubscribed`](/docs/guides/email/events#emaillistunsubscribed) event. ## An in-body unsubscribe link The other path is a plain unsubscribe link in the message body, the visible opt-out that marketing mail is expected to carry on top of the header button. Bird adds one to the HTML of your `marketing` sends automatically, so a recipient reading the HTML always has a link to click without you building or hosting anything. You are free to add your own link as well (for example to a preference center you host), and the platform link remains as the compliant floor. When a recipient unsubscribes through the in-body link, Bird fires an [`email.unsubscribed`](/docs/guides/email/events#emailunsubscribed) event, distinct from `email.list_unsubscribed` so you can tell a footer-link opt-out apart from a provider-button one. ## The webhook events Bird delivers both unsubscribe events to your [webhook endpoint](/docs/guides/webhooks) in the standard event envelope. Subscribe to them the same way you subscribe to any other email event — there is no separate unsubscribe webhook to configure: | Event | Fires when | | :------------------------ | :------------------------------------------------------------------------------- | | `email.unsubscribed` | The recipient clicked the unsubscribe link in your message body. | | `email.list_unsubscribed` | The recipient used the mailbox provider's one-click button (`List-Unsubscribe`). | Both carry the identity base every email event carries — `email_id`, `recipient_id`, `workspace_id`, the `recipient` address and its `recipient_role`, plus the `tags` and `metadata` from the original send — so you can reconcile the opt-out against your own records without an extra lookup: ```json { "type": "email.list_unsubscribed", "timestamp": "2026-06-10T14:30:00Z", "data": { "email_id": "em_01krdgeqcxet5s7t44vh8rt9mg", "recipient_id": "er_01krdgeqcxet5s7t44vh8rt9mg", "workspace_id": "ws_01krdgeqcxet5s7t44vh8rt9mg", "recipient": "subscriber@example.com", "recipient_role": "to", "tags": [{ "name": "category", "value": "newsletter" }], "metadata": { "list_id": "weekly-digest" } } } ``` Use these events to update preferences in your own system — flip the subscription off, log the opt-out, stop including the address in your sends. The field set and the rest of the email event catalog are in the [events reference](/docs/guides/email/events). ## Suppression closes the loop You don't have to act on the webhook to stop Bird from mailing an unsubscribed recipient again — Bird does it automatically. Both `email.unsubscribed` and `email.list_unsubscribed` add the address to your workspace [suppression list](/docs/guides/email/suppressions) with reason `unsubscribe` and `applies_to: non_transactional`: - Future **`marketing`** sends to the address are rejected up front with `email.rejected` and `rejection_reason: recipient_suppressed`. - Future **`transactional`** sends still go through — an unsubscribe is a preference about marketing mail, not a dead address. That makes the webhook events a mirror for your own database rather than the thing that protects your reputation; Bird already blocks the send. If you ever need to inspect or undo an unsubscribe, the suppression record carries `source_email_id` and `source_recipient_id` linking back to the message that caused it, and you can remove it through the [suppressions API](/docs/guides/email/suppressions#managing-suppressions-via-the-api). ## Next steps - [Categories](/docs/guides/email/categories) — why `marketing` is what makes an unsubscribe take effect - [Suppressions](/docs/guides/email/suppressions) — the list unsubscribes land on, and how to manage it - [Email events](/docs/guides/email/events) — the full `email.unsubscribed` / `email.list_unsubscribed` payloads and the rest of the catalog - [Gmail & Yahoo requirements](/docs/knowledge-base/deliverability/gmail-yahoo-requirements) — where one-click unsubscribe is required for bulk senders - [Webhooks](/docs/guides/webhooks) — subscribing an endpoint, signature verification, and retries --- title: "Errors" description: "The Bird error envelope — stable error codes, coarse types, validation details, vendor codes, and how to branch on failures correctly." source: https://bird.com/en-us/docs/guides/errors --- # Errors Every failed Bird API request returns the same JSON envelope, nested under a top-level `error` key: ```json { "error": { "type": "validation_error", "code": "E01001", "name": "ValidationError", "message": "Request validation failed.", "doc_url": "https://bird.com/docs/api/errors/E01001", "request_id": "req_01krdgeqcxet5s7t44vh8rt9mg", "details": [ { "param": "to", "message": "must contain at least one recipient" }, { "param": "subject", "message": "exceeds maximum length of 998 characters" } ] } } ``` The HTTP status code is set correctly at the transport layer — `400` for bad input, `401`/`403` for auth, `404` for missing resources, `409` for conflicts, `429` for rate limits, `5xx` for Bird-side failures — and the body adds specificity. Status alone tells you the category; the envelope tells you exactly what happened. ## The envelope fields | Field | Role | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | Broad category for coarse branching — `validation_error`, `auth_error`, `permission_error`, `not_found_error`, `conflict_error`, `rate_limit_error`, `billing_error`, `internal_error`, and a handful of others. A closed enum that grows rarely. | | `code` | Opaque, stable identifier (`E01001`). **The canonical reference** — unique, never renamed, never reused. When an error is retired, its code is permanently reserved. | | `name` | Human-readable slug (`ValidationError`) for log readability. Always paired with `code`, never a replacement for it. | | `message` | Human-readable description. **Not stable** — wording changes without notice. Display it, log it, never parse it. | | `param` | For input-related errors, the offending field. Omitted when not applicable. | | `doc_url` | Permanent link to the docs page for this code. Valid forever, even for retired codes. | | `request_id` | Always present, and also returned as the `X-Request-Id` response header. Quote it in support requests — it lets Bird trace the exact request. | **Branch on `type` for coarse handling and `code` for specific handling — never on `message`.** A typical client switches on `type` (retry on `rate_limit_error`, surface `validation_error` to the user, page someone on `internal_error`) and matches individual `code` values only for the few errors it handles specially. ```bash curl -s https://us1.platform.bird.com/v1/email/messages \ -X POST \ -H "Authorization: Bearer bk_us1_invalid" \ -H "Content-Type: application/json" \ -d '{}' | jq '.error | {type, code, request_id}' ``` The opacity of codes is deliberate: a code like `E04012` doesn't pretend to be self-documenting, so every code gets a real docs page (its `doc_url`) describing the exact cause and what to do about it. The full catalog lives in the [error reference](/docs/api/errors). ## Two structured exceptions ### Validation failures: one code, many details Field-level validation does not get a separate code per field-and-failure combination. Every validation failure is `E01001 ValidationError` with a `details` array listing each field-level problem as `{param, message}` — see the envelope example above. The `details` array is present only on `validation_error` responses, and the `message` strings inside it are human-readable, not machine-stable: use `param` to map problems to form fields, use the message for display. ### External-system failures: `vendor_code` When a failure originates in a system outside Bird — an upstream email provider, an SMS carrier, a card network, a recipient's mail server — the envelope carries one stable Bird `code` plus a `vendor_code` field with the external system's own code, verbatim: ```json { "error": { "type": "internal_error", "code": "E05003", "name": "SparkPostError", "message": "An error occurred communicating with the mail infrastructure provider.", "doc_url": "https://bird.com/docs/api/errors/E05003", "request_id": "req_01krdgeqcxet5s7t44vh8rt9mg", "vendor_code": "1902" } } ``` This keeps Bird's catalog small and stable while still giving you the full upstream taxonomy when you need it — the docs page for each such code explains what its `vendor_code` values mean in context. ## Handling errors well - **Retry `429` and `5xx`, nothing else by default.** Honor `Retry-After` on rate limits (see [Rate limits](/docs/guides/rate-limits)), use exponential backoff on `5xx`, and send an [`Idempotency-Key`](/docs/guides/idempotency) so retries of mutating requests are safe. - **Log `code`, `name`, and `request_id` together.** The code is what you'll search the docs and your own logs for; the request ID is what support needs. - **Tolerate new codes and types.** New error codes ship regularly as products grow, and the `type` enum occasionally gains a value — write your handler with a sane default branch rather than an exhaustive match. ## Next steps - [Error reference](/docs/api/errors) — the full error catalog, one page per code - [Idempotency](/docs/guides/idempotency) — safe retries for mutating requests - [Rate limits](/docs/guides/rate-limits) — limit headers and backoff guidance --- title: "Idempotency" description: "Safe retries with the Idempotency-Key header — replay semantics, key scoping, conflict errors, and what happens on server failures." source: https://bird.com/en-us/docs/guides/idempotency --- # Idempotency Networks fail at the worst moments: you POST a send, the connection drops, and now you don't know whether the email went out. Idempotency lets you retry that request safely — send the same `Idempotency-Key` header again and Bird replays the original response instead of processing the request a second time. ## How it works Idempotency is **opt-in**. Add an `Idempotency-Key` header to any `POST`, `PATCH`, or `DELETE` request; requests without the header are processed normally with no deduplication. `GET` requests are naturally idempotent and ignore the header. ```bash curl -X POST https://us1.platform.bird.com/v1/email/messages \ -H "Authorization: Bearer bk_us1_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \ -d '{ "from": "hello@example.com", "to": ["user@example.com"], "subject": "Welcome!", "html": "<p>Thanks for signing up.</p>" }' ``` A key is any non-empty string up to 255 characters. A UUID v4 is the recommended format; a deterministic key derived from your own entities (e.g. `welcome-user/usr_abc123`) also works well when you want retries across process restarts to share a key. The [Bird SDKs](/docs/sdks/concepts) generate a UUID key automatically for every mutating request, so SDK users get safe retries without doing anything. Keys are **scoped to your workspace** — two workspaces can use the same key string without colliding — and retained for roughly **3 hours**. A retry after the retention window is processed as a fresh request, so the window comfortably covers typical retry schedules (seconds to an hour) but is not a permanent dedupe log. ## Replays When Bird sees a key it has already completed, it returns the cached response — same status code, same body — without re-running the request. Both `2xx` and `4xx` responses are cached and replayed: if your first attempt failed validation, retrying the identical request returns the identical error rather than burning another attempt. Replayed responses carry one extra header so you can tell them apart from fresh processing: ```http HTTP/1.1 202 Accepted Idempotency-Replay: true ``` ## Failure modes | Scenario | Response | | ------------------------------------------------ | -------------------------------------------------------- | | Same key, same request | Cached response replayed with `Idempotency-Replay: true` | | Same key, **different** request body or endpoint | `409` — `E01005 IdempotencyKeyReuse` | | Same key, original request still in flight | `409` — `E01004 RequestInProgress` | | Empty key, or key longer than 255 characters | `400` — `E01002 InvalidRequest` | Reusing a key with a different request is treated as a client bug — Bird returns `409` immediately rather than silently handing you a response that doesn't match what you sent. Generate a new key for the new request. The comparison covers the method, endpoint, path and query parameters, and the raw request body, so even a whitespace change in the body counts as a different request. `RequestInProgress` means a concurrent request with the same key hasn't finished yet — typically an aggressive client-side timeout retrying while the first attempt is still processing. The in-flight lock expires automatically within about 30 seconds, so wait briefly and retry. See [Errors](/docs/guides/errors) for the error envelope these come wrapped in. ## What's not cached **`5xx` responses are never cached.** A server error means Bird doesn't know whether the request took effect, and caching it would permanently block the retry from ever succeeding. Instead the key unlocks, and your retry is processed fresh. This is exactly the case idempotency exists for: retry `5xx` responses and timeouts with the same key, and you'll either get the original successful response (if the first attempt actually completed) or a clean new attempt. **Email sends get an extra durable guarantee.** For `POST /v1/email/messages` and the batch endpoint, acceptance is additionally deduplicated in durable storage, independent of the response cache. Even if the cache misses — a lock expired, a response was too large to cache — a retry with the same key replays the same accepted email IDs rather than accepting the send twice. Your retries never produce duplicate emails. See [Sending email](/docs/guides/email/sending-email). ## Practical guidance - Generate one key per logical operation, not per HTTP attempt — the whole point is that retries of the same operation share the key. - Retry on network errors, timeouts, and `5xx` with exponential backoff, reusing the same key each time. - Treat `409 IdempotencyKeyReuse` as a bug in your key generation, not something to retry. - Don't bother with keys on `GET` requests or on operations that are naturally idempotent in your domain — the mechanism is there for the cases where a duplicate would hurt. ## Next steps - [Idempotency API reference](/docs/api/idempotency) — header and response-header schemas - [SDK concepts](/docs/sdks/concepts) — automatic key generation and retry behavior in the SDKs - [Errors](/docs/guides/errors) — the error envelope and code catalog - [Sending email](/docs/guides/email/sending-email) — send and batch endpoints --- title: "Rate limits" description: "How Bird API rate limiting works — org-level limits, endpoint groups, the base/tier/override resolution model, IETF rate-limit headers, and how to handle 429s." source: https://bird.com/en-us/docs/guides/rate-limits --- # Rate limits Every Bird API endpoint is rate limited. Limits exist to keep the platform stable for everyone — they are generous enough that well-behaved integrations rarely hit them, and every response tells you exactly where you stand so you never have to guess. This page describes the model, which is the same across all channels. The actual numbers live on per-channel pages — see [Email rate limits](/docs/guides/email/rate-limits). ## How limits are keyed Your quota is shared across everything you send: every authenticated request counts against one account-wide quota, regardless of which API key or workspace made it. (Quotas are organization-level, so multiple keys or workspaces draw from the same allowance.) Unauthenticated endpoints (login, signup, password reset) are keyed by client IP instead, with fixed abuse-prevention thresholds that are not adjustable. ## Groups Endpoints are grouped into **rate limit groups**, and the group sets the limit. A group typically covers endpoints with a similar cost profile: single-resource reads share one group, collection queries another, management writes a third, and expensive operations like sending get groups of their own. When you hit a limit, only that group is exhausted — running out of send quota does not block you from reading delivery status or managing webhooks. The group that matched your request is named in the rate-limit response headers, so you always know which bucket you exhausted. ## How your limit is resolved Your effective limit for a group is resolved from three layers: 1. **Base** — the default that applies to every organization. 2. **Tier** — your subscription tier can raise the limit for specific groups. A tier never lowers a limit below base. 3. **Override** — a per-organization override, arranged with Bird, for customers whose volume outgrows their tier's ceiling. An active override takes precedence over the base and tier values. In practice: the base is the floor, your tier raises it, and an override is the escape hatch when you need more. If your traffic is approaching your tier's ceiling, contact support — overrides are a normal part of scaling on Bird, not an exception process. You can list your organization's current effective limits with [`GET /v1/organization/rate-limits`](/docs/api). ## Response headers Every response from a rate-limited endpoint carries two headers in the IETF Structured Fields format ([RFC 9651](https://www.rfc-editor.org/rfc/rfc9651)): ```text RateLimit-Policy: "email_send";q=1000;w=60 RateLimit: "email_send";r=842;t=35 ``` | Header | Meaning | | ------------------ | ----------------------------------------------------------------------------------------------------------- | | `RateLimit-Policy` | The policy that applies: `q` is the quota (max requests) and `w` is the window in seconds. | | `RateLimit` | Your current state: `r` is the number of requests remaining and `t` is the seconds until the window resets. | The quoted string names the group that matched the request. In the example above, the org has an effective `email_send` limit of 1000 requests per 60 seconds, with 842 remaining and 35 seconds until reset. Bird uses this format instead of the older `X-RateLimit-*` convention because it is unambiguous — `t` is always seconds from now, never a Unix timestamp — and because a single response can express multiple policies if an endpoint is ever subject to more than one. ## When you hit a limit An exhausted limit returns `429 Too Many Requests` with a `Retry-After` header (in seconds, matching the `t` value) and the standard [error envelope](/docs/guides/errors): ```text HTTP/1.1 429 Too Many Requests Retry-After: 35 RateLimit-Policy: "email_send";q=1000;w=60 RateLimit: "email_send";r=0;t=35 Content-Type: application/json ``` ```json { "type": "rate_limit_error", "code": "E01003", "message": "Too many requests. Please retry after the period indicated in the Retry-After header.", "request_id": "req_abc123" } ``` Branch on `type: rate_limit_error`, not the message text. The group name and retry timing are in the headers, not the body: ```python import time import requests def send_with_backoff(url, headers, payload, max_attempts=5): for attempt in range(max_attempts): response = requests.post(url, headers=headers, json=payload) if response.status_code != 429: return response retry_after = int(response.headers.get("Retry-After", 2 ** attempt)) time.sleep(retry_after) raise RuntimeError("rate limited after max retries") ``` A rate-limited request is rejected before it does any work — it does not consume an [idempotency key](/docs/guides/errors), so retrying with the same key is always safe. The [Bird SDKs](/docs/sdks/concepts) handle all of this automatically: they detect `429`s, honor `Retry-After`, and retry with backoff, so most SDK users never write this code. ## Failure mode The rate limiter **fails open**: if its backing store is unavailable, requests are allowed rather than spuriously refused. Rate limiting is a protective measure, not a security boundary — authentication and authorization are always enforced regardless of the limiter's health. You will never receive a `429` caused by a Bird-side outage. ## Per-channel limits - [Email](/docs/guides/email/rate-limits) — send and batch groups, management traffic, and tier ceilings. ## Next steps - [Errors](/docs/guides/errors) — the error envelope and how to branch on error types - [Rate limits API reference](/docs/api) — list your organization's effective limits - [SDK concepts](/docs/sdks/concepts) — automatic retry and backoff behavior --- title: "SMS events reference" description: "The catalog of SMS events Bird emits — the message lifecycle, per-event payload fields, and how to read events over webhooks or the events API." source: https://bird.com/en-us/docs/guides/sms/events --- # SMS events reference Every message moves through a lifecycle, and Bird emits an event at each step. This page is the full event vocabulary; how events are delivered to your endpoint — signatures, retries, replay — is covered in the [Webhooks guide](/docs/guides/webhooks). The delivery lifecycle, as a path through the event types: - `sms.accepted` — Bird has the message and is preparing to hand it to a carrier - → `sms.sent` — the message was handed to the carrier; awaiting a delivery receipt - → `sms.delivered` — the carrier confirmed delivery to the handset - → or `sms.undelivered` — a non-permanent non-delivery (handset off, unreachable, content blocked) - → or `sms.failed` — a permanent failure; the message will not be delivered - → or `sms.rejected` — Bird refused the message before it reached a carrier - → or `sms.expired` — the message's validity window elapsed before a terminal receipt arrived Each message ends in exactly one terminal status — `delivered`, `undelivered`, `failed`, `rejected`, or `expired`. Inbound messages surface through `sms.received` instead of this outbound path. The event `type` is an **open enum**: Bird may add new event types over time, so treat an unrecognized `type` as a future event rather than an error. Match the types you handle and ignore the rest. ## The event envelope Events arrive at your webhook endpoint in the [Standard Webhooks](https://www.standardwebhooks.com) nested envelope described in the [Webhooks guide](/docs/guides/webhooks) — three fields: `type`, `timestamp`, and a type-specific `data` object. The event's identity is not in the body: it rides in the `webhook-id` HTTP header, which is stable across retries of the same delivery and is your deduplication key. | Field | Description | | ----------- | ----------------------------------------------------------------------------------------------------------- | | `type` | One of the event types on this page, e.g. `sms.delivered` | | `timestamp` | When the event occurred (RFC 3339) — sort by this, never by arrival order, since deliveries are not ordered | | `data` | Event-specific payload (fields below) | Every SMS event's `data` carries the identity base: `sms_id`, `workspace_id`, the `to` and `from` addresses, and the `tags` and `metadata` you set on the send — echoed on every event so you can route and correlate against your own records without an extra lookup (each is `null` when the send carried none). ```json { "type": "sms.delivered", "timestamp": "2026-06-10T14:30:00Z", "data": { "sms_id": "sms_01krdgeqcxet5s7t44vh8rt9mg", "workspace_id": "ws_01krdgeqcxet5s7t44vh8rt9mg", "to": "+15551234567", "from": "Bird", "carrier": "Example Wireless", "mcc_mnc": "310260", "tags": [{ "name": "campaign", "value": "spring-2026" }], "metadata": { "order_id": "ord_123" } } } ``` ## Lifecycle events ### sms.accepted Fires when Bird accepts the send and begins preparing to hand it to a carrier. This is the first event in every outbound message's stream. Payload: the identity base only. ### sms.sent Fires when Bird has handed the message to the carrier and is awaiting a delivery receipt. Payload: the identity base only — to measure processing latency, compare this event's `timestamp` with `sms.accepted`'s. ### sms.delivered The carrier confirmed the message reached the handset. Payload adds `carrier` and `mcc_mnc` — the delivering network and its mobile country/network code. ## Failure events ### sms.undelivered A non-permanent non-delivery — the handset was off or unreachable, or the carrier blocked the content. Payload adds an `error` object with the reason. ### sms.failed A permanent delivery failure; the message will not be delivered. Payload adds an `error` object with the reason. ### sms.rejected Bird refused the message before it ever reached a carrier — a validation or policy rejection, so no delivery was attempted. This is distinct from a failure at the carrier: a rejection is the message never getting that far. Payload adds an `error` object with the reason. ### sms.expired The message's validity window elapsed before a terminal delivery receipt arrived. Payload: the identity base only. ## The events API The same per-message events are queryable after the fact with `GET /v1/sms/messages/{message_id}/events`. It returns the message's events in chronological order, in full — the timeline is bounded, so it is not paginated. Each event carries: | Field | Description | | ------------- | ---------------------------------------------------------------------------------------------------- | | `id` | The event ID (`evt_` prefix) | | `type` | The event type, one of those above | | `occurred_at` | When the event occurred (RFC 3339) | | `carrier` | The delivering carrier, when known (present on delivery events) | | `mcc_mnc` | The mobile country/network code, when known | | `error` | The failure detail (description and code), present on `undelivered`, `failed`, and `rejected` events | Use the events API to backfill, replay, or reconcile against your webhook deliveries by `sms_id`. It's the same stream the [SMS log](/docs/guides/sms/sms-log)'s per-message timeline is rendered from. ## Next steps - [Webhooks & events](/docs/guides/webhooks) — endpoint setup, signature verification, retries, and replay - [SMS log](/docs/guides/sms/sms-log) — the per-message timeline these events drive - [Sending SMS](/docs/guides/sms/sending-sms) — the send payload whose `tags` and `metadata` echo on every event --- title: "SMS overview" description: "What Bird SMS is, how sending works, and where to find everything in this section." source: https://bird.com/en-us/docs/guides/sms/overview --- # SMS overview Bird SMS sends text messages through the same platform and the same API keys as [Bird Email](/docs/guides/email/overview). You call Bird with a `bk_{region}_…` key against your regional host (`https://us1.platform.bird.com` or `https://eu1.platform.bird.com`); Bird picks a route to the recipient's carrier, splits your text into billable segments, and reports each message through to a delivery receipt. One API, one set of credentials, per-channel endpoints under `/v1/sms/…`. SMS is in early access and rolls out per workspace. If your workspace isn't enabled yet, the SMS endpoints and the dashboard's SMS pages return a not-authorized error; the rest of this section describes how they behave once you're in. ## The SMS app in the dashboard In the [dashboard](/docs/knowledge-base/getting-started/dashboard-tour), SMS is one of the workspace's channel apps. Its pages, and where each one's guide lives: | Page | What it's for | | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | [**Messages**](https://bird.com/dashboard/w/sms/messages) | Every message sent and received, with its per-message event timeline and delivery detail — see the [SMS log](/docs/guides/sms/sms-log) | | [**Metrics**](https://bird.com/dashboard/w/sms/metrics) | Delivery rate, failure rate, accepted volume, segments, and spend over time — see [Tracking & metrics](/docs/guides/sms/tracking-and-metrics) | ## How sending works You send an SMS with `POST /v1/sms/messages`: one recipient, one body, one message. Bird validates the request, prices and queues it, and returns `202 Accepted` with a message ID — delivery happens asynchronously from there. A `202` means Bird has durably accepted your send and is working on it; the actual outcome (sent to the carrier, delivered, undelivered, failed, expired) arrives afterwards through [events and webhooks](/docs/guides/sms/events) and the message read endpoints. To send many messages in one call, `POST /v1/sms/batches` takes up to 100 independent messages. Three ideas shape the whole API: - **A send is not a delivery.** The `202` means Bird accepted the message, not that a handset received it. Each message walks its own lifecycle — accepted, sent to the carrier, then a terminal receipt — and the delivery receipt can arrive seconds or minutes later. - **Text is billed in segments.** A message longer than one segment (160 GSM-7 characters, or 70 if it contains emoji or other non-GSM characters) is split and billed per segment. [Sending SMS](/docs/guides/sms/sending-sms#segments-and-encoding) explains how the split works and how to keep messages to one segment. - **Why you send matters.** Every message carries a [category](/docs/guides/sms/sending-sms#body-and-category) — `transactional`, `marketing`, `authentication`, or `service` — which drives opt-out (STOP) handling, quiet-hours rules, and per-country compliance. ## Sending [Sending SMS](/docs/guides/sms/sending-sms) covers the send API in depth: the recipient and sender formats, the body and its segment cap, categories, tags and metadata, the async `202` model, batch sends, and idempotent retries. ## Visibility Every message produces an event timeline — accepted, sent, delivered, or a failure — plus the segment count and cost once it's priced: - **[SMS log](/docs/guides/sms/sms-log)** — the per-message view in the dashboard: every send and receipt, its event timeline, segments, cost, and carrier detail. - **[Tracking & metrics](/docs/guides/sms/tracking-and-metrics)** — the aggregate delivery rate, failure rate, accepted volume, segment count, spend, and delivery latency across everything you send. - **[Events](/docs/guides/sms/events)** — the SMS event vocabulary, the message lifecycle, and webhook delivery to your own endpoints. ## Receiving Inbound messages — replies and messages sent to your number — appear in the [SMS log](/docs/guides/sms/sms-log) with a `received` status and an `inbound` direction, alongside your outbound traffic. Filter the log by direction to see one side at a time. ## Next steps | Page | What it covers | | ----------------------------------------------------------- | -------------------------------------------------------------------- | | [Sending SMS](/docs/guides/sms/sending-sms) | The send API — recipients, senders, body, segments, categories, tags | | [SMS log](/docs/guides/sms/sms-log) | Every message, its event timeline, segments, and cost | | [Tracking & metrics](/docs/guides/sms/tracking-and-metrics) | Delivery rate, failure rate, segments, spend, and latency | | [Events](/docs/guides/sms/events) | The SMS event types and webhook delivery | | [Webhooks & events](/docs/guides/webhooks) | Endpoint setup, signature verification, retries, and replay | --- title: "Sending SMS" description: "Send a text message with POST /v1/sms/messages — recipient and sender formats, the body and its segment cap, categories, tags vs metadata, batch sends, and idempotent retries." source: https://bird.com/en-us/docs/guides/sms/sending-sms --- # Sending SMS This guide covers the single-send endpoint, `POST /v1/sms/messages`. You build one JSON payload with a recipient, a body, and a category; Bird returns `202 Accepted` with a message ID and delivers asynchronously. Each request sends one message to one recipient — to send many at once, use [batch sending](#batch-sending). ## A minimal send The smallest valid free-text payload is a `to` recipient, a `text` body, and a `category`. ```bash curl -X POST https://us1.platform.bird.com/v1/sms/messages \ -H "Authorization: Bearer bk_us1_..." \ -H "Content-Type: application/json" \ -d '{ "to": "+15551234567", "text": "Your Bird verification code is 481920.", "category": "authentication" }' ``` Use your regional host (`https://us1.platform.bird.com` or `https://eu1.platform.bird.com`) with a matching `bk_{region}_...` key. Omitting `from` lets Bird auto-select a sender that's valid for the destination country; set it explicitly to send from a specific number, sender ID, or short code (see [Sender](#sender)). ## Building up the payload ### Recipient `to` is a single recipient in [E.164](https://en.wikipedia.org/wiki/E.164) format — a leading `+`, country code, and subscriber number, e.g. `+15551234567`. One message goes to one recipient; there is no `cc`/`bcc` and no recipient array. To reach many people, send a [batch](#batch-sending). ### Sender `from` is the sender the recipient sees. It can be one of three shapes, and which ones are allowed depends on the destination country and your account's registered senders: - **A phone number** in E.164 (a long code or toll-free number you've provisioned). - **An alphanumeric sender ID** — up to 11 characters (e.g. `Bird`). Supported in many countries but not all; where it isn't, delivery falls back or fails per local rules. Alphanumeric senders are one-way — recipients can't reply. - **A short code** — a 5–6 digit number. Omit `from` and Bird selects an eligible sender for the destination automatically. ### Body and category `text` is the message body, at least one character. It is billed and delivered in [segments](#segments-and-encoding); a send is capped at **12 segments** (about 1,836 GSM-7 characters, or 804 if the body uses the extended UCS-2 encoding). A body over the cap is rejected with a `422` rather than truncated. `category` is required on a free-text send and classifies the message: `transactional`, `marketing`, `authentication`, or `service`. The category drives opt-out (STOP) handling, quiet-hours enforcement, and per-country compliance — so a one-time passcode (`authentication`) and a promotion (`marketing`) are treated differently by the rules that govern when and whether Bird may deliver them. Choose the category that matches the message's purpose. ### Tags and metadata Both attach your own data to a send, but they serve different jobs: - **`tags`** are structured `{name, value}` pairs (max 20 per send; name 1–32 characters, value 1–64, ASCII `[A-Za-z0-9_-]` only, case-sensitive). They are first-class filter dimensions — filter the [message log](/docs/guides/sms/sms-log) by tag and slice metrics by tag. Use them for low-cardinality labels like `campaign`, `template`, or `experiment_variant`. - **`metadata`** is an arbitrary JSON object (max 2 KB serialized). It is stored, returned on API reads, and echoed on every [webhook event](/docs/guides/sms/events) — but it is not a filter dimension. Use it for round-trip context: internal IDs, foreign keys, anything you want handed back with each event. ```json { "tags": [{ "name": "campaign", "value": "spring-2026" }], "metadata": { "user_id": "usr_12345", "order_id": "ord_98765" } } ``` ### Field reference | Field | Type | Required | Limits / notes | | :--------- | :---------------- | :------- | :----------------------------------------------------------------------------------------- | | `to` | string (E.164) | yes | One recipient per message | | `from` | string | no | E.164 number, alphanumeric sender ID (≤ 11 chars), or short code; auto-selected if omitted | | `text` | string | yes\* | At least 1 character; capped at 12 [segments](#segments-and-encoding) | | `category` | string | yes\* | `transactional`, `marketing`, `authentication`, or `service` | | `tags` | `{name, value}[]` | no | Max 20; name 1–32 chars, value 1–64 chars; `[A-Za-z0-9_-]` only | | `metadata` | object | no | Arbitrary JSON, max 2 KB serialized | \* A free-text send requires `text` and `category`. Sending from a pre-registered template is a separate path that supplies both from the template. ## Segments and encoding SMS carries a fixed number of characters per segment, and the limit depends on the encoding Bird picks for your body: - **GSM-7** — the default 7-bit alphabet (Latin letters, digits, common punctuation). A single-segment message fits **160** characters; longer messages are split at **153** characters per segment (the split reserves a few characters for reassembly headers). - **UCS-2** — used automatically when the body contains any character outside GSM-7, such as an emoji, a CJK character, or some accented letters. The limits drop to **70** characters single-segment and **67** per segment when split. One out-of-range character switches the whole message to UCS-2 and more than halves its capacity, so a stray "smart quote" or emoji can turn a one-segment message into two. Each message read reports the resolved `segments`: the billable `count`, the `encoding` (`GSM_7BIT` or `UCS2`), and the character `count`. Segments are the unit you're billed on — see [cost](#cost-and-billing). ## Batch sending `POST /v1/sms/batches` sends up to 100 independent messages in one request. The body is an array of the same message objects described above: ```bash curl -X POST https://us1.platform.bird.com/v1/sms/batches \ -H "Authorization: Bearer bk_us1_..." \ -H "Content-Type: application/json" \ -d '{ "messages": [ { "to": "+15551234567", "text": "Order shipped.", "category": "transactional" }, { "to": "+15557654321", "text": "Order shipped.", "category": "transactional" } ] }' ``` Validation is all-or-nothing: if any message in the batch is invalid, the whole request is rejected with a `422` and nothing is sent, so a batch never partially applies. On success the response returns each accepted message plus a `summary` with the `accepted_count`. Each message in the batch is independent from there — one recipient's failure never affects the others. ## The async model: what 202 means A successful send returns `202 Accepted` with a message ID and `status: accepted`. The `202` is returned only after the send is durably accepted — it is never accepted and then silently dropped. Hard failures you can fix (an invalid recipient, a body over the segment cap, a missing category) fail immediately with a `422`; a send that would exceed your workspace balance fails with a `402`. Actual delivery happens asynchronously: the message moves to `sent` when Bird hands it to the carrier, then to a terminal status (`delivered`, `undelivered`, `failed`, or `expired`) when the receipt arrives, reported through [events and webhooks](/docs/guides/sms/events) and the read endpoints. Two consequences of the async design worth knowing: - **Cost is priced after acceptance.** The `cost` on a message is `null` at accept time and is populated once Bird prices the send during processing. Read the message back (or wait for the delivery event) to see the final segment count and charge. - **Reads can briefly trail the 202.** There is a short window between the `202` and the message becoming visible on the read endpoints; a `404` immediately after a send resolves itself within moments. ## Reserved fields The following fields are part of the request vocabulary but are not yet available. Including any of them returns `422 unsupported_feature` — they are reserved now so SDKs and integrations converge on one name before launch: `scheduled_at`, `validity_period`, `media_urls`, `messaging_profile_id`, `broadcast_id`, `campaign_id`, `audience_id`, `contact_id`, `topic_id`, `max_price_per_segment`, `personalization`, `track_clicks` Don't build around these yet. ## Retrying safely Send the `Idempotency-Key` header with a unique value per logical send, and retries become safe: if your first request succeeded but you never saw the response (timeout, dropped connection), replaying the same request with the same key returns the original result instead of sending a duplicate message. See [idempotency](/docs/guides/idempotency) for key format and retention. ## Cost and billing Outbound SMS is billed per segment. The final charge depends on the destination country and carrier — some routes add a surcharge (for example, US 10DLC carrier fees). Each single-message read includes a `cost` breakdown: the `per_segment` price, the billed `segment` count, the destination `country_code`, and any `carrier_surcharge`. Because pricing happens during processing, the cost is `null` until the message is priced. Aggregate spend and segment counts across a range are on the [Metrics](/docs/guides/sms/tracking-and-metrics) page. ## Next steps - [SMS log](/docs/guides/sms/sms-log) — find a message, follow its lifecycle, and read its segments and cost - [Events](/docs/guides/sms/events) — the per-message event stream behind the log and webhooks - [Tracking & metrics](/docs/guides/sms/tracking-and-metrics) — delivery rate, failure rate, segments, and spend - [Idempotency](/docs/guides/idempotency) — safe retries with the `Idempotency-Key` header --- title: "SMS log" description: "Browse, filter, and inspect every message your workspace sent and received from the Messages page: statuses, the per-message event timeline, and segment and cost detail." source: https://bird.com/en-us/docs/guides/sms/sms-log --- # SMS log The **SMS log** — the [**Messages**](https://bird.com/dashboard/w/sms/messages) page in the Bird dashboard — is your workspace's record of every message it has sent and received: every send made through [`POST /v1/sms/messages`](/docs/guides/sms/sending-sms) lands here, newest first, alongside inbound messages. Use it to confirm a message went out, see where it is in the delivery lifecycle, and read its segment count, cost, and carrier detail. It is the per-message companion to the aggregate [Metrics](/docs/guides/sms/tracking-and-metrics) page — Metrics tells you the rates across everything you send, the SMS log lets you drill into a single message. For a tour of where this page sits in the dashboard, see the [dashboard tour](/docs/knowledge-base/getting-started/dashboard-tour). ![The SMS Messages page in the Bird dashboard: a table of sent messages with status (Delivered, Undelivered), sender, recipient, message text, a category badge (AUTH, TXN, MKTG, SVC), and sent time, above a recipient search box and status and date filters](/images/docs/dashboard-sms-messages.png) ## The message list Each row is one message. The columns are: | Column | What it shows | | :----------- | :---------------------------------------------------------------------------------------------------------------------------- | | **Status** | The message's current delivery status (see [Statuses](#statuses) below), shown as a color-coded indicator | | **From** | The sender — the number, sender ID, or short code the message was sent from | | **To** | The recipient's number | | **Message** | The start of the message body (click it, or anywhere on the row, to open the message) | | **Category** | The message [category](/docs/guides/sms/sending-sms#body-and-category), shown as a short badge (`TXN`, `MKTG`, `AUTH`, `SVC`) | | **Sent** | When the send was accepted, as a relative time (hover for the exact timestamp) | The list is paginated, 25 messages per page; use **Prev** and **Next** to move between pages. ## Searching and filtering A message log fills up fast, so the page leads with a recipient search and four filters. They combine — a status filter plus a date range narrows to messages matching both. **Search by recipient.** The search box matches an **exact recipient number** in E.164 format — type the full number you sent to (e.g. `+15551234567`) to find every message addressed to it. It is an exact match, not a substring search. **Status.** Filter to one or more delivery statuses — status is **multi-select**, so you can, say, show everything that didn't arrive by selecting `Undelivered`, `Failed`, and `Expired` together. The options are the statuses listed below. **Category.** Filter by message category: `Transactional`, `Marketing`, `Authentication`, or `Service`. **Direction.** Filter by `Outbound` (messages you sent) or `Inbound` (messages received from a subscriber). **Date.** Filter by when the message was sent — pick a preset or choose a custom range from the calendar. When a filter combination matches nothing, the page shows a no-results state with a **Clear filters** action to reset back to the full list. ## Statuses A message's status is where it currently sits in the [delivery lifecycle](/docs/guides/sms/events). Outbound messages walk from `accepted` toward a terminal receipt; inbound messages arrive as `received`. | Status | Meaning | | :-------------- | :------------------------------------------------------------------------------------------ | | **Scheduled** | Queued to send at a future time | | **Accepted** | Bird admitted the message and is preparing to hand it to a carrier | | **Sent** | Handed to the carrier; awaiting a delivery receipt | | **Delivered** | The carrier confirmed delivery to the handset | | **Undelivered** | A non-permanent non-delivery — the handset was off, unreachable, or the content was blocked | | **Failed** | A permanent failure; the message will not be delivered | | **Rejected** | Bird refused the message before it reached a carrier (a validation or policy rejection) | | **Canceled** | A scheduled message that was canceled before it sent | | **Expired** | The message's validity window elapsed before a terminal receipt arrived | | **Received** | An inbound message from a subscriber | The indicator is color-coded: green for delivered and received, blue for in-flight (`Accepted`, `Sent`, `Scheduled`), a warning color for `Undelivered` and `Expired`, and a destructive color for `Failed`, `Rejected`, and `Canceled`. ## Inspecting a message Click any row to open the message in a side panel. It has two tabs — **Events** and **Details** — and its header shows the message body, the current status, the recipient, and the category. While a message is still in flight, the panel refreshes itself every few seconds, so a status change or a delivery receipt appears without a manual reload. ![The SMS message detail sheet in the Bird dashboard: the Events tab showing the per-message lifecycle timeline — Accepted, Sent after 221 ms, and Delivered after 2.6 s on carrier EE — over the dimmed message list](/images/docs/dashboard-sms-detail.png) ### Events The default tab is a timeline of everything that happened to the message, in order, each with its timestamp. This is the same event stream described in the [SMS events reference](/docs/guides/sms/events), rendered for one message: `Accepted` → `Sent` → `Delivered`, or a failure event (`Undelivered`, `Failed`, `Rejected`, `Expired`). Delivery events show the `carrier` and `MCC/MNC` (the mobile network) they were confirmed on; failure events carry their error description and code inline, so a failed message tells you why on its own timeline. ### Details The **Details** tab is the message's metadata: - **Message ID**, **Direction**, **From**, **To**, and **Category** - **Segments** — the billable segment `count`, the `encoding` (`GSM_7BIT` or `UCS2`), and the character count (see [segments and encoding](/docs/guides/sms/sending-sms#segments-and-encoding)) - **Cost** — the amount and currency, once the message has been priced - **Carrier** and **MCC/MNC** — the delivering network, when known - **Validity period** — the window the message stays valid for delivery, in seconds - **Sent**, **Handed to carrier**, and **Delivered** timestamps - **Error** — the description and code, on a message that didn't arrive - Any **Tags** and **Metadata** you set on the send Most fields can be copied with one click. ## Next steps - [Tracking & metrics](/docs/guides/sms/tracking-and-metrics) — aggregate delivery, failure, segment, and spend numbers across everything you send - [SMS events reference](/docs/guides/sms/events) — the full event vocabulary the timeline is built from - [Sending SMS](/docs/guides/sms/sending-sms) — the send payload, including categories, tags, and metadata --- title: "SMS templates" description: "Browse the SMS template catalogue in the dashboard, read a template's category, languages, and variables, and send by template reference instead of free text." source: https://bird.com/en-us/docs/guides/sms/templates --- # SMS templates A template is a pre-registered message body you send by reference instead of inlining the text on every call. It carries its own category and a set of typed variable slots, so a send only supplies the values that change (an OTP code, an order number) and Bird assembles the final message. The catalogue you can send from includes Bird's built-in **system** templates and any templates authored for your **workspace**. Sending from a template also settles compliance up front: the message category comes from the template, so opt-out (STOP) handling, quiet-hours, and per-country rules are decided by the template rather than by each caller. ## Browsing templates in the dashboard The [**Templates**](https://bird.com/dashboard/w/sms/templates) tab, under SMS, lists every template available to your workspace. Search by name and filter by status or category to find one. ![The SMS Templates tab: a search box with Status and Category filters above a table of templates, each row showing a name, an Active status, a category, an EN language chip, and a System scope, across Name, Status, Category, Language, Scope, and Updated columns.](/images/docs/dashboard-sms-templates.png) Each row shows the fields you need to pick and send a template: - **Name** — the human-readable title, with the template's stable reference (for example `order_notification`) below it. The reference is what you pass when sending; it never changes. - **Status** — the template's lifecycle state. **Active** templates are live and sendable; **Draft**, **Pending**, **Approved**, and **Rejected** track a workspace template through review. Built-in system templates are always Active. - **Category** — the content classification (`transactional`, `marketing`, `authentication`, or `service`) applied to every message sent from the template. - **Language** — the languages the template is stocked in, as BCP 47 tags. The first few are shown as chips with a `+N` overflow when a template is localized into many languages. - **Scope** — **System** for Bird's built-in templates, **Workspace** for ones your workspace authored. - **Updated** — when the template last changed. Built-in templates show no date. ## What's in a template Beyond the columns above, each template defines the **variables** it fills in at send time. A variable is a typed slot with a `key` (the name you supply a value under), a `type`, a `required` flag, and a human-readable `constraint` describing the accepted values. When you send, you supply a value for every required variable and no undeclared keys — a missing required variable or an unknown key is rejected. A template is stocked in one or more **languages**. You pick one per send; if you request a language the template isn't stocked in, Bird falls back to the closest available language and then to English rather than failing. ## Listing templates from the API `GET /v1/sms/templates` returns the catalogue in full — it is small and not paginated. Filter by `scope`, `category`, or `language` to narrow it: ```bash curl https://us1.platform.bird.com/v1/sms/templates?category=authentication \ -H "Authorization: Bearer bk_us1_..." ``` Each template in the response carries its `name` (the reference you send by), `scope`, `category`, `status`, `available_languages`, the `variables` it expects, and a `body` preview of the default-language text. Fetch a single template by its reference with `GET /v1/sms/templates/{template_ref}` to read its variables before a send. ## Sending with a template Set the send's `template` object instead of `text`. The two are mutually exclusive, and because the category comes from the template, you omit `category` (and `from` is chosen for you unless you set it): ```bash curl -X POST https://us1.platform.bird.com/v1/sms/messages \ -H "Authorization: Bearer bk_us1_..." \ -H "Content-Type: application/json" \ -d '{ "to": "+15551234567", "template": { "name": "order_notification", "language": "en", "parameters": { "order_id": "98765", "eta": "Tuesday" } } }' ``` `name` is the template reference from the catalogue. `language` selects the localized body (omit for English). `parameters` supplies a value for each of the template's variables, keyed by variable name. Every required variable must be present and no undeclared key may appear, or the send is rejected with a `422`. Everything else about the send — recipients, senders, tags, metadata, and the async `202` model — works exactly as it does for a [free-text send](/docs/guides/sms/sending-sms). ## Next steps - [Sending SMS](/docs/guides/sms/sending-sms) — the full send payload that the `template` field slots into - [SMS log](/docs/guides/sms/sms-log) — find a sent message and follow its lifecycle - [Events](/docs/guides/sms/events) — the per-message event stream behind the log and webhooks --- title: "Tracking & metrics" description: "How to read your aggregate SMS metrics in the Bird dashboard — delivery rate, failure rate, accepted volume, segments, spend, and delivery latency." source: https://bird.com/en-us/docs/guides/sms/tracking-and-metrics --- # Tracking & metrics The **Metrics** page in the Bird dashboard is where you see how your SMS is doing: how much of it reached the handset, whether your failure rate is drifting toward trouble, how many segments you're sending, and what it's costing you. This guide walks through that page — what each number means and when to act on it. Metrics are the aggregate view across everything your workspace sends. For the lifecycle of a single message — did _this_ number receive it — see the [SMS log](/docs/guides/sms/sms-log) and [events](/docs/guides/sms/events). ## Read your metrics The [**Metrics**](https://bird.com/dashboard/w/sms/metrics) page lives at **SMS → Metrics** in the Bird dashboard. Every number respects the range picker in the top right (last 24 hours, 7, 30, or 90 days) and updates as new events arrive, so a report you open now reflects sends and delivery receipts up to the last few seconds. Rates are computed against **event time, not send time** — a delivery receipt that arrives today for a message accepted yesterday lands in today's numbers. `accepted` is the denominator for both rate cards: it's the count of messages Bird admitted in the range, and every rate is measured against it. ![The SMS Metrics page in the Bird dashboard: the delivery-rate (Healthy, 97%), failure-rate (2.58% of a 5% limit), accepted-volume, and segments-and-spend summary tiles above the delivery-over-time chart](/images/docs/dashboard-sms-metrics.png) ### The summary tiles The row of tiles at the top is your at-a-glance health check: - **Delivery rate** — delivered messages as a share of accepted. Bird marks it Healthy at or above **95%**; below that, something is failing to reach handsets (bad numbers, blocked content, or routing problems) and the failure breakdown below tells you which. The tile also shows the change versus the previous period. - **Failure rate** — the share of accepted messages that ended `undelivered`, `failed`, or `expired`. The tile shows your rate against a **5% limit** with a progress bar; crossing it flips the tile to Risk. A sustained high failure rate usually means list-quality or content problems. - **Accepted** — the raw count of messages accepted in the range, with the number handed on to carriers (`sent`) and the change versus the previous period. - **Segments** — the total billable [segments](/docs/guides/sms/sending-sms#segments-and-encoding) sent and the spend behind them, with the change versus the previous period. Because a message can be several segments, this is usually higher than your accepted count — a gap that widens means longer messages or a shift to the UCS-2 encoding. The 95% and 5% thresholds are the guardrails Bird uses to color the tiles. They're deliberately conservative — a tile can read Healthy and still have room to improve. ### Delivery over time The delivery chart plots **accepted**, **delivered**, and **failed** volume across the range so you can spot trends and one-off spikes — a bad campaign, a number-list import gone wrong, a route that degraded. The `failed` series here rolls up every non-delivery outcome (undelivered, failed, and expired) into one line. The bucket size follows the range (hourly for 24 hours, daily for longer windows). ### Failure rate and its causes Below the chart, a failure-rate line charts the per-bucket failure rate across the range, and an outcome histogram breaks your non-deliveries down by cause, so a rising failure rate points you at the fix: - **Undelivered** — a non-permanent non-delivery: the handset was off, unreachable, or the content was blocked by the carrier. - **Failed** — a permanent failure; the message will not be delivered. - **Expired** — the message's validity window elapsed before a delivery receipt arrived. - **Rejected** — Bird refused the message before it reached a carrier (a validation or policy rejection). Each bucket shows its count and its share of accepted messages. One thing to note when reconciling the numbers: the **Failure rate** tile counts `undelivered`, `failed`, and `expired`, while `rejected` is shown in this histogram but sits outside the failure-rate math — a rejected message never reached a carrier, so it's tracked separately from delivery failures. ### Delivery latency The latency table shows how long messages take to move through the pipeline, split into **Processing** (Bird accepting the message and handing it to a carrier), **Delivery** (the carrier delivering to the handset), and **Total** (end to end), each at the p50, p95, and p99 percentiles. Use p95/p99 to catch the slow tail — a healthy median with a slow p99 usually points at one carrier or destination lagging. Latency is a whole-window figure; where a percentile has no data for the range it shows `—`. ![The lower half of the SMS Metrics page in the Bird dashboard: the failure outcome histogram (undelivered, failed, expired, and rejected counts) above the delivery-latency table, with p50, p95, and p99 columns for the processing, delivery, and total stages](/images/docs/dashboard-sms-metrics-failures.png) ## The stats API The dashboard reads the same numbers you can pull programmatically: - `GET /v1/sms/stats/summary` — the single-row aggregate behind the tiles: the lifecycle counts, `delivery_rate`, `failure_rate`, the latency percentiles, and `spend` (total segments and cost) for a range. Pass `compare=previous_period` to get the deltas the tiles show. - `GET /v1/sms/stats/daily` and `GET /v1/sms/stats/hourly` — one row of lifecycle counts per day or hour, for charting over time. Daily covers up to a year; hourly up to 30 days. Each endpoint can slice the range by one dimension per request — `originator`, `country`, `category`, or `carrier` — and takes a `timezone` so day and hour buckets line up with your reporting calendar. ## Next steps - [SMS log](/docs/guides/sms/sms-log) — the per-message view behind the aggregate numbers - [Events](/docs/guides/sms/events) — the per-message event stream the metrics are derived from - [Sending SMS](/docs/guides/sms/sending-sms) — categories, tags, and the segment model behind segment and spend counts --- title: "Users, teams & roles" description: "How people get access in Bird — workspace roles and what each can do, the org roles that manage billing, managing the team, and smart invitations." source: https://bird.com/en-us/docs/guides/users-teams-roles --- # Users, teams & roles Access for people in Bird is role-based: a user holds a role on your workspace, and each role is a fixed set of permissions. There is nothing else to configure — pick the right role and the permissions follow. Roles govern what _people_ can do in the dashboard. What _services_ can do is governed by [API key scopes](/docs/guides/authentication) — the same permission vocabulary, granted per key instead of per role. ## Workspace roles The roles you'll work with day to day live on the **workspace** (see [Workspaces](/docs/guides/workspaces)): `admin`, `developer`, and `analyst`. Manage them in the dashboard under [**Settings → Team**](https://bird.com/dashboard/w/settings/team). Every permission is a `{scope, level}` pair, where `level` is `read` or `write` (`write` includes `read`). A role is simply a named, fixed set of these pairs. | Scope | What `write` means | `admin` | `developer` | `analyst` | | ------------------ | ---------------------------------------------- | ------- | ----------- | --------- | | `workspace` | Edit workspace settings (name, icon, timezone) | write | read | read | | `api_keys` | Create and revoke API keys | write | write | — | | `emails` | Send email | write | write | read | | `email_management` | Manage suppressions and email configuration | write | write | read | | `domains` | Add, verify, and remove sending domains | write | write | read | | `webhooks` | Configure webhook endpoints | write | write | read | | `ip_pools` | View the organization's IP pools (read-only) | read | read | read | | `members` | Manage the workspace team and invitations | write | read | read | | `analytics` | View reports and deliverability analytics | read | — | read | | `audit` | View the audit log | read | — | read | | `request_logs` | View the request log (read-only) | read | read | read | In practice: **admin** runs the workspace (team, settings, and everything a developer can do), **developer** builds the integration (send email with `emails:write`, manage domains with `domains:write`, configure webhooks with `webhooks:write`, mint API keys), and **analyst** is read-only everywhere. Note that `ip_pools` is read-only even for admins — purchasing dedicated IPs and managing pools is an organization-level operation (`org:ip_pools:write`), because pools are owned by the organization and shared across workspaces. Other channels add their own scopes (for example `numbers` for phone numbers); they all follow the same `{scope, level}` model. A `403` from any endpoint means the authenticated principal lacks the `{scope, level}` that endpoint requires — the fix is a role change (for a person) or a new key with the right scopes (for a service). ### Organization roles Behind your workspace sits an organization that owns billing and the overall member list. Two roles manage that layer: - `owner` — everything. Full read and write across all organization operations and every workspace, present and future, with no workspace role needed. An organization can (and should) have multiple owners. The person who created the account starts as owner. - `billing_admin` — billing and organization settings (`org:billing:write`, `org:settings:write`) plus read on members and workspaces (`org:members:read`, `org:workspaces:read`). No access to workspace operations. These rarely come up day to day: most teammates only need a workspace role. ## Members and the team Membership is implicit: a user is "in" your organization if they hold any role — an organization role, or a workspace role on any workspace. There is no separate membership record to manage. You manage the people on your workspace in the dashboard under **Settings → Team**, gated by the workspace `members` scope; a workspace admin manages their own team here without any organization-level role. Behind it, the cross-workspace view of everyone in the organization (with their org role and all workspace roles) is available via [`/v1/organization/members`](/docs/api), gated by `org:members`. Team management is something people do, so API keys cannot hold the `members` scopes (see [Authentication](/docs/guides/authentication)). ![The workspace Team settings in the Bird dashboard, listing members with their role and the invite-members action](/images/docs/dashboard-settings-team.png) Removing someone from a workspace removes only that workspace role — they remain an organization member if they have roles elsewhere. Removing someone from the organization (`DELETE /v1/organization/members/{member_id}`) revokes everything: their organization role and all their workspace roles at once. API keys they created keep working either way — keys belong to the workspace, not the person. ## Invitations Inviting is workspace-first: you invite an email address to a workspace with a role, and Bird figures out the rest. This is a _smart invitation_ — the same request handles both colleagues who are already in your organization and people who have never heard of Bird: ```bash curl -X POST https://us1.platform.bird.com/v1/invitations \ -H "Authorization: Bearer <your-credential>" \ -H "X-Workspace-Id: <workspace-id>" \ -H "Content-Type: application/json" \ -d '{ "email": "dana@yourcompany.com", "role": "developer" }' ``` - **Already an organization member** — they are added to the workspace immediately with the given role. No email, no waiting; the response is a member object. - **Not yet a member** — Bird creates an invitation, emails them a signup link, and the response is an invitation object with a `pending` status. The link is valid for **7 days**; after that the invitation expires and must be re-sent. The response carries a `type` discriminator (`team_member` or `invitation`) so you can tell which happened. A pending invitation for the same email returns `409` rather than creating a duplicate, and you can withdraw one at any time with `POST /v1/invitations/{invitation_id}/revoke`. Organization-level invitations (`POST /v1/organization/invitations`) are the multi-workspace variant: one invitation can carry an organization role (`owner` or `billing_admin`), workspace roles on several workspaces, or both. You can never grant more than you hold — inviting someone as an owner requires being an owner. ## Guardrails Two invariants are enforced on every role change, regardless of who asks: - **The last owner is immovable.** Demoting or removing an organization's only owner returns `409` — an organization can never end up ownerless. Promote a second owner first. - **You cannot change your own access.** Changing your own role or removing yourself returns `403`. This prevents both accidental self-lockout and quiet self-promotion; another admin or owner has to make the change. ## How context is selected Member and team endpoints exist at two levels, and which organization or workspace a request targets depends on how it authenticates: - **Session auth** (the dashboard, or tools acting as you) selects context explicitly per request: `X-Organization-Id` on organization-scoped endpoints, `X-Workspace-Id` on workspace-scoped ones. - **API keys** carry their context implicitly — a key belongs to exactly one workspace, which also pins the organization. No headers needed; sending a context header that contradicts the key returns `403`. See [Workspaces](/docs/guides/workspaces) for the full context-resolution model. ## Next steps - [Authentication & API keys](/docs/guides/authentication) — scopes for services, and who can manage keys - [Workspaces](/docs/guides/workspaces) — the workspace these roles attach to - [Members reference](/docs/api) — full endpoint and schema documentation --- title: "Webhooks & events" description: "Receive Bird events as signed HTTPS deliveries — endpoint setup, Standard Webhooks signature verification, retries, replay, and the event catalog." source: https://bird.com/en-us/docs/guides/webhooks --- # Webhooks & events When something happens in your workspace — an email is delivered, a recipient bounces, a sending domain verifies — Bird POSTs a signed JSON event to every webhook endpoint you have subscribed to that event type. Bird follows the [Standard Webhooks](https://www.standardwebhooks.com) specification for headers, signing, and payload structure, so if you already verify webhooks from another Standard Webhooks platform, the same verification code works here unchanged. ## Create an endpoint Register an endpoint in the dashboard under [**Developers → Webhooks**](https://bird.com/dashboard/w/webhooks), or via [`POST /v1/webhooks`](/docs/api/reference/create-webhook). Endpoint URLs must be HTTPS, and Bird rejects URLs that resolve to private, loopback, link-local, or otherwise internal addresses — the check runs at creation time and again at every delivery, so DNS tricks can't redirect deliveries to an internal target. ![The Webhooks page in the Bird dashboard, listing an active endpoint with its subscribed events](/images/docs/dashboard-webhooks.png) ```bash curl -X POST https://us1.platform.bird.com/v1/webhooks \ -H "Authorization: Bearer bk_us1_..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/webhooks/bird", "events": ["email.delivered", "email.bounced", "email.complained"], "description": "Production delivery + bounce notifications" }' ``` The `events` array is an explicit list of event types from the [catalog below](#event-catalog) — an endpoint receives only the types it lists, and you can change the list at any time with `PATCH /v1/webhooks/{webhook_id}` (the new filter applies to future deliveries). To receive everything, subscribe to every type; check back here when new event types ship, since subscriptions never expand on their own. The `201` response includes the endpoint's signing `secret` (prefixed `whsec_`) **exactly once** — store it in your secret manager immediately. It cannot be retrieved again; if you lose it, [rotate it](#rotating-the-signing-secret). ```json { "id": "whk_01krdgeqcxet5s7t44vh8rt9mg", "url": "https://example.com/webhooks/bird", "events": ["email.delivered", "email.bounced", "email.complained"], "description": "Production delivery + bounce notifications", "status": "active", "secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw" } ``` Endpoints support full CRUD: `GET /v1/webhooks` lists them, `GET /v1/webhooks/{webhook_id}` fetches one, `PATCH` updates any field, and `DELETE` removes the endpoint and stops all deliveries. A workspace can register multiple endpoints, each with its own URL, event filter, and secret. ## Verify signatures Every delivery carries three headers: | Header | Value | | ------------------- | ----------------------------------------------------------------------------- | | `webhook-id` | Stable ID for this delivery — automatic retries of the same delivery reuse it | | `webhook-timestamp` | Unix timestamp (seconds) of the delivery attempt | | `webhook-signature` | `v1,<base64 HMAC-SHA256>` — may contain multiple space-delimited signatures | The signature is an HMAC-SHA256 over the string `{webhook-id}.{webhook-timestamp}.{raw request body}`, keyed with your endpoint's secret (strip the `whsec_` prefix and base64-decode the remainder to get the key bytes). Your handler should verify the signature, reject deliveries whose `webhook-timestamp` is more than 5 minutes old, and deduplicate on `webhook-id` — Bird delivers at-least-once, so the same delivery can arrive more than once. With the [Bird SDK](/docs/sdks/typescript), all three checks are one call: <!-- bird:snippet webhook.unwrap --> ```typescript // Pass the RAW request body; set the secret via new BirdClient({ webhooks: { secret } }). const event = bird.webhooks.unwrap(rawBody, headers); console.log(event.type); // discriminated union — narrow on event.type ``` Any [Standard Webhooks reference library](https://www.standardwebhooks.com) works too. If you verify by hand, the recipe is: ```typescript import { createHmac, timingSafeEqual } from "node:crypto"; function verify(rawBody: string, headers: Record<string, string>, secret: string): boolean { const id = headers["webhook-id"]; const timestamp = headers["webhook-timestamp"]; if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // 5-minute tolerance const key = Buffer.from(secret.slice("whsec_".length), "base64"); const expected = createHmac("sha256", key) .update(`${id}.${timestamp}.${rawBody}`) .digest("base64"); // The header can hold several signatures (e.g. during secret rotation) — accept if any matches. return headers["webhook-signature"].split(" ").some((part) => { const sig = Buffer.from(part.replace(/^v1,/, ""), "base64"); return ( sig.length === Buffer.byteLength(expected, "base64") && timingSafeEqual(sig, Buffer.from(expected, "base64")) ); }); } ``` Always compute the HMAC over the **raw request body bytes** — parsing and re-serializing the JSON will change whitespace or key order and break the signature. ## Delivery semantics Each delivery is one event per HTTP POST with `Content-Type: application/json` — no batching. Your endpoint has **5 seconds** to respond; any `2xx` status counts as success, and anything else (including `3xx` redirects and timeouts) counts as a failure. Respond quickly and process asynchronously — queue the event and return `200` before doing real work. Failed deliveries are retried up to 10 attempts with exponential backoff (plus a little jitter so retries don't synchronize): | Attempt | Delay after previous failure | | ------- | ---------------------------- | | 1 | Immediate | | 2 | 5 seconds | | 3 | 30 seconds | | 4 | 2 minutes | | 5 | 10 minutes | | 6 | 30 minutes | | 7 | 1 hour | | 8 | 2 hours | | 9 | 4 hours | | 10 | 8 hours | All retries of a delivery carry the same `webhook-id`, which is what makes deduplication work. After the tenth attempt the delivery is permanently failed — the source event is retained and can be [replayed](#replaying-failed-events), and a replay is a new delivery with a **new** `webhook-id`. Deliveries are **not ordered**. An `email.delivered` can arrive before the `email.accepted` for the same message, especially when retries are involved — sort by the `timestamp` field inside the event payload, never by arrival order. ## Operate your endpoints ### Test sends `POST /v1/webhooks/{webhook_id}/test` sends a synthetic, fully-signed event to your endpoint and returns the outcome synchronously — whether your endpoint accepted it, the HTTP status it returned, and the round-trip latency. Pass `{"event_type": "email.delivered"}` to simulate a specific event type, or omit the body for a generic test ping. An unreachable endpoint is reported in the response body, not as a request error, so you can use this to debug connectivity. For end-to-end testing with real event flows, send to the [sandbox addresses](/docs/guides/email/testing-sandbox) — sandbox sends emit real webhook events through the normal delivery path, which is the best way to exercise your handler before going live. ### Replaying failed events `POST /v1/webhooks/{webhook_id}/replay` queues failed events for redelivery — pass `since`/`until` timestamps to bound the window (default: the last 24 hours). The request returns `202` and events are redelivered asynchronously; each replayed event is a new delivery with a new `webhook-id`, and the standard retry schedule applies if it fails again. `GET /v1/webhooks/{webhook_id}/attempts` lists recent delivery attempts with status codes and latency when you need to see what failed and why. ### Rotating the signing secret `POST /v1/webhooks/{webhook_id}/rotate-secret` generates a new secret and returns it once. For the next **24 hours** every delivery is signed with both the old and the new secret — the `webhook-signature` header carries both signatures space-delimited (`v1,<old> v1,<new>`), so you can deploy the new secret without dropping a single event. Standard Webhooks libraries try all signatures automatically. After 24 hours the old secret stops signing. ### Auto-pause and re-enable Endpoint `status` is `active`, `degraded`, or `paused`. Sustained delivery failures mark an endpoint `degraded` (a health warning — delivery continues, and the status clears itself when deliveries succeed again), and an endpoint that keeps failing for several days is automatically `paused`: all delivery stops and you're notified by email. A paused endpoint never resumes on its own — re-enable it with `PATCH /v1/webhooks/{webhook_id}` and `{"status": "active"}` (or from the [**Webhooks**](https://bird.com/dashboard/w/webhooks) page in the dashboard) once it's healthy. ## Event catalog Event payloads are compact, recipient-scoped facts: enough to know what happened and correlate it to your system (`email_id` + `recipient_id` + `workspace_id`), not the full resource. If you need more context, fetch the email by `email_id`. Per-event payload schemas live in the [email events reference](/docs/guides/email/events). | Event | When it fires | | --------------------------- | ------------------------------------------------------------------------------------------ | | `email.accepted` | Bird accepted the send and is preparing to deliver | | `email.processed` | Bird processed the message and queued it for delivery | | `email.rejected` | A recipient was rejected before the delivery pipeline (e.g. suppressed) | | `email.delivered` | The recipient's mail server accepted the message | | `email.deferred` | Temporary delivery failure — Bird is retrying | | `email.bounced` | Permanent delivery failure for a recipient | | `email.out_of_band_bounce` | A late bounce arrived after the message was already delivered | | `email.complained` | The recipient marked the message as spam | | `email.opened` | The tracking pixel loaded | | `email.clicked` | A tracked link was clicked | | `email.unsubscribed` | The recipient unsubscribed via a footer or link | | `email.list_unsubscribed` | RFC 8058 one-click list-unsubscribe | | `email.received` | Bird received an inbound email — see [Receiving email](/docs/guides/email/receiving-email) | | `domain.verified` | A sending domain passed DNS verification | | `domain.failed` | A previously verified domain failed a re-check | | `email_suppression.created` | An address was automatically suppressed after a bounce, complaint, or unsubscribe | Every delivery body is the Standard Webhooks nested envelope — exactly three fields: a `type`, a `timestamp`, and a type-specific `data` object. The event's identity rides in the `webhook-id` header, not the body. The envelope `timestamp` is when the event **occurred** (for `email.delivered`, the moment the receiving server accepted the message) — distinct from the `webhook-timestamp` header, which is the time of this delivery attempt and changes on every retry. ```json { "type": "email.delivered", "timestamp": "2026-06-10T14:30:00Z", "data": { "email_id": "em_01krdgeqcxet5s7t44vh8rt9mg", "recipient_id": "er_01krdgeqcxet5s7t44vh8rt9mg", "workspace_id": "ws_01krdgeqcxet5s7t44vh8rt9mg", "recipient": "user@example.com", "recipient_role": "to", "tags": [{ "name": "category", "value": "welcome" }], "metadata": { "order_id": "ord_123" } } } ``` Every email event's `data` carries this identity base (`email_id`, `recipient_id`, `workspace_id`, the `recipient` address, and its envelope `recipient_role`) plus the `tags` and `metadata` from the send request (`null` when not provided); event types with more to say extend it with type-specific fields. Within a variant the field set is stable — fields are required by default, and presence never varies by anything other than the event type. Event names follow `resource.action` and are never renamed; new types are added as products ship, so write your handler to ignore types it doesn't recognize. ## Next steps - [Webhooks API reference](/docs/api/reference/create-webhook) — full endpoint and schema documentation - [Email events reference](/docs/guides/email/events) — per-event payload fields - [Testing & sandbox](/docs/guides/email/testing-sandbox) — sandbox sends drive real webhook deliveries, ideal for testing handlers --- title: "WhatsApp events" description: "The lifecycle events a WhatsApp message emits — accepted, sent, delivered, read, failed — and how to read them from the events API." source: https://bird.com/en-us/docs/guides/whatsapp/events --- # WhatsApp events Every WhatsApp message emits **events** as it moves through its lifecycle. Each event is a stamped fact about the message: when Bird accepted it, when it reached WhatsApp, when it was delivered, when the recipient read it, or that it failed. Events are how you follow what happened to a message after the send returns `202`. ## The lifecycle A message emits events in order as it progresses. Not every message emits every type — a failed send stops at `whatsapp.failed`, and `whatsapp.read` only appears if the recipient opens the message. | Event | Meaning | | -------------------- | ---------------------------------------------------------------- | | `whatsapp.accepted` | Bird accepted the send request. This is what the `202` reported. | | `whatsapp.sent` | Handed to the WhatsApp network. | | `whatsapp.delivered` | Delivery confirmed to the recipient's device. | | `whatsapp.read` | The recipient opened the message. | | `whatsapp.failed` | Terminal, permanent failure. Carries the reason in `error`. | Two things about `whatsapp.read`: it does **not** change the message `status` (a delivered message stays `delivered`; the read is recorded as a `read_at` timestamp), and it only fires when read receipts are available for that conversation. ## Reading events from the API `GET /v1/whatsapp/messages/{message_id}/events` returns a message's events, oldest first. Reading events needs an API key with the `whatsapp_messages` scope: ```bash curl https://us1.platform.bird.com/v1/whatsapp/messages/wamsg_.../events \ -H "Authorization: Bearer bk_us1_..." ``` A message that was accepted, sent, delivered, and read returns four events: ```json { "data": [ { "id": "ev_01kx43q96rfm3r1t70pgfqj9sa", "type": "whatsapp.accepted", "occurred_at": "2026-07-09T18:54:55.962Z", "error": null }, { "id": "ev_01kx43qb29f9qvxrf5xxm9ft95", "type": "whatsapp.sent", "occurred_at": "2026-07-09T18:54:57.415Z", "error": null }, { "id": "ev_01kx43qe1nfbt9vkt6xhw7v00m", "type": "whatsapp.delivered", "occurred_at": "2026-07-09T18:54:59Z", "error": null }, { "id": "ev_01kx43qkzzes8vb8r2mjqa8avh", "type": "whatsapp.read", "occurred_at": "2026-07-09T18:55:05Z", "error": null } ] } ``` Each event carries an `id`, a `type`, the `occurred_at` timestamp, and an `error`. The `error` is `null` on every event except `whatsapp.failed`, where it holds the failure detail. The same timeline is what the [Messages](/docs/guides/whatsapp/messages) page renders when you open a message. ![The WhatsApp message detail sheet in the Bird dashboard, opened for a delivered bird_order_confirmation message: the Events tab showing the per-message lifecycle timeline — Accepted, Sent, Delivered, and Read, each with its timestamp — over the dimmed message list](/images/docs/dashboard-whatsapp-detail.png) ## Webhooks Webhook events are **coming soon**. Until then, read events from the API above. ## Next steps - [WhatsApp messages](/docs/guides/whatsapp/messages) — the per-message view that renders this timeline - [Sending WhatsApp messages](/docs/guides/whatsapp/sending-whatsapp) — where a message's lifecycle begins --- title: "WhatsApp messages" description: "Browse, filter, and inspect every WhatsApp message your workspace sends from the Messages page: statuses, the per-message event timeline, and delivery detail." source: https://bird.com/en-us/docs/guides/whatsapp/messages --- # WhatsApp messages The [**Messages**](https://bird.com/dashboard/w/whatsapp/messages) page in the Bird dashboard is your workspace's record of every WhatsApp message it has sent: every send made through [`POST /v1/whatsapp/messages`](/docs/guides/whatsapp/sending-whatsapp) lands here, newest first. Use it to confirm a message went out, see where it is in the delivery lifecycle, and read which template produced it. For a tour of where this page sits in the dashboard, see the [dashboard tour](/docs/knowledge-base/getting-started/dashboard-tour). ## The message list ![The WhatsApp Messages page in the Bird dashboard: a table of sent messages with Status (Sent, Failed, Delivered), From and To phone numbers, the Template name, a Category badge (Authentication, Utility), and a relative Sent time, above an exact-phone-number search box and Status and Date filters](/images/docs/dashboard-whatsapp-messages.png) Each row is one message. The columns are: | Column | What it shows | | :----------- | :------------------------------------------------------------------------------------ | | **Status** | The message's current status (see [Statuses](#statuses) below), as a colored dot | | **From** | The WhatsApp sender number the message was sent from | | **To** | The recipient's number | | **Template** | The template the message was sent from (click the row to open the message) | | **Category** | The template's [category](/docs/guides/whatsapp/sending-whatsapp#category-and-sender) | | **Sent** | When the send was accepted, as a relative time (hover for the exact timestamp) | The list is paginated, 25 messages per page; use **Prev** and **Next** to move between pages. ## Searching and filtering A message log fills up fast, so the page leads with a recipient search and two filters. They combine — a status filter plus a date range narrows to messages matching both. **Search by recipient.** The search box matches an **exact recipient number** in [E.164](https://en.wikipedia.org/wiki/E.164) format — type the full number you sent to (e.g. `+15551234567`) to find every message addressed to it. It is an exact match, not a substring search. **Status.** Filter to one or more statuses — status is **multi-select**, so you can, say, show everything still in flight by selecting `Accepted` and `Sent` together. The filterable options are `Accepted`, `Sent`, `Delivered`, and `Failed`. **Date.** Filter by when the message was sent — pick a preset or choose a custom range from the calendar. When a filter combination matches nothing, the page shows a no-results state with a **Clear filters** action to reset back to the full list. ## Statuses A message's status is where it currently sits in the [lifecycle](/docs/guides/whatsapp/events). A message walks from `accepted` toward a terminal receipt. | Status | Meaning | | :------------ | :---------------------------------------------------------------- | | **Accepted** | Bird admitted the message and is preparing to hand it to WhatsApp | | **Sent** | Handed to WhatsApp; awaiting a delivery receipt | | **Delivered** | WhatsApp confirmed delivery to the recipient | | **Failed** | The message will not be delivered | A read receipt is not a status: when a recipient reads a delivered message, Bird records a `read_at` timestamp and a `whatsapp.read` [event](/docs/guides/whatsapp/events) on the message, and the delivery status stays `delivered`. Two more statuses, `scheduled` and `canceled`, are reserved for a future scheduled-send feature and aren't emitted today. ## Inspecting a message Click any row to open the message. Its header shows the recipient, the template, and the current status; while a message is still in flight, the view refreshes itself every few seconds, so a status change or a delivery receipt appears without a manual reload. ![The WhatsApp message detail sheet in the Bird dashboard, opened for a delivered bird_order_confirmation message: the Events tab showing the per-message lifecycle timeline — Accepted, Sent, Delivered, and Read, each with its timestamp — over the dimmed message list](/images/docs/dashboard-whatsapp-detail.png) - **Events** — a timeline of everything that happened to the message, in order, each with its timestamp: `Accepted` → `Sent` → `Delivered`, the `whatsapp.read` receipt if the recipient read it, or a failure. This is the same stream you can read with `GET /v1/whatsapp/messages/{message_id}/events`. - **Details** — the message's metadata: its ID, the sender and recipient numbers, the template (name, language, and category), the `created`, `sent`, `delivered`, and `read` timestamps, and, on a message that didn't arrive, the error description and code. ## Reading messages from the API `GET /v1/whatsapp/messages` lists your messages, newest first; `GET /v1/whatsapp/messages/{message_id}` reads a single message, and `GET /v1/whatsapp/messages/{message_id}/events` returns its event timeline. Reading messages needs an API key with the `whatsapp_messages` scope. ## Next steps - [Sending WhatsApp messages](/docs/guides/whatsapp/sending-whatsapp) — the send payload behind every row - [WhatsApp events](/docs/guides/whatsapp/events) — the per-message lifecycle stream behind the timeline - [WhatsApp pricing](https://bird.com/pricing/whatsapp) — what a message costs, by destination and category --- title: "WhatsApp overview" description: "What Bird WhatsApp is, how template sending works, and where to find everything in this section." source: https://bird.com/en-us/docs/guides/whatsapp/overview --- # WhatsApp overview Bird WhatsApp sends messages through the same platform and the same API keys as [Bird Email](/docs/guides/email/overview) and [Bird SMS](/docs/guides/sms/overview). You call Bird with a `bk_{region}_…` key against your regional host (`https://us1.platform.bird.com` or `https://eu1.platform.bird.com`); Bird sends the message to WhatsApp, and reports it through to a delivery receipt. One API, one set of credentials, per-channel endpoints under `/v1/whatsapp/…`. Business-initiated WhatsApp is **template-only**: every send names a pre-approved [message template](https://bird.com/dashboard/w/whatsapp/templates) and supplies its variables. There is no free-text send yet. > **Billing** — Bird charges for a WhatsApp message when it is **sent**, regardless of whether it is ultimately delivered. The price depends on the destination country and the template's category; see [WhatsApp pricing](https://bird.com/pricing/whatsapp). ## The WhatsApp app in the dashboard In the [dashboard](/docs/knowledge-base/getting-started/dashboard-tour), WhatsApp is one of the workspace's channel apps. Its pages: | Page | What it's for | | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | [**Messages**](https://bird.com/dashboard/w/whatsapp/messages) | Every message you send, with its per-message event timeline and delivery detail | | [**Templates**](https://bird.com/dashboard/w/whatsapp/templates) | The pre-approved templates you can send — name, language, category, and a rendered preview of each | | [**Numbers**](https://bird.com/dashboard/w/whatsapp/numbers) | The WhatsApp sender numbers available to your workspace | ## How sending works You send a WhatsApp message with `POST /v1/whatsapp/messages`: one recipient, one template. Bird validates the request and returns `202 Accepted` with a message ID; pricing, the charge, and delivery all happen asynchronously from there. A `202` means Bird has durably accepted your send and is working on it; the actual outcome (sent to WhatsApp, delivered, read, or failed) arrives afterwards through events, webhooks, and the message read endpoints. Unlike SMS and email, there is no batch endpoint — send one call per message. Three ideas shape the whole API: - **A send is not a delivery.** The `202` means Bird accepted the message, not that a phone received it. Each message walks its own lifecycle — accepted, sent to WhatsApp, then a terminal receipt — and the receipt can arrive seconds or minutes later. A read receipt is reported separately as a `read_at` timestamp and a `whatsapp.read` event, not as a status. - **Every send uses a template (for now).** Business-initiated WhatsApp currently requires a pre-approved template; other message types will follow. You supply the template's `name`, its `language`, and `text` values for any `{{n}}` variables it declares — see [Sending WhatsApp messages](/docs/guides/whatsapp/sending-whatsapp). - **Category and destination drive sender and price.** Each template carries a category — `authentication`, `utility`, or `marketing`. Bird selects the sender number from that category (there is no `from` field), and pricing is keyed on the category together with the recipient's country — see [WhatsApp pricing](https://bird.com/pricing/whatsapp). ## Sending [Sending WhatsApp messages](/docs/guides/whatsapp/sending-whatsapp) covers the send API in depth: the recipient, the template name and language, filling template variables through components, the async `202` model, what isn't supported yet, and idempotent retries. ## Visibility Every message produces an event timeline — accepted, sent, delivered, read, or a failure: - **[Messages](https://bird.com/dashboard/w/whatsapp/messages)** — the per-message view in the dashboard: every send, its event timeline, and the template that produced it. - **[Events](/docs/guides/whatsapp/events)** — the per-message lifecycle stream behind the log. ## Events Every WhatsApp message emits events as it moves through its lifecycle — accepted, sent to WhatsApp, delivered, read, or a failure. Read a message's events with `GET /v1/whatsapp/messages/{message_id}/events`; see [WhatsApp events](/docs/guides/whatsapp/events) for the full event list and payload. Receiving them on your own endpoint through webhooks is coming soon. ## Receiving Inbound messages are coming soon. ## Next steps | Page | What it covers | | ------------------------------------------------------------------- | ------------------------------------------------------------------------ | | [Sending WhatsApp messages](/docs/guides/whatsapp/sending-whatsapp) | The send API — recipient, template, components, the async model, retries | | [Templates](/docs/guides/whatsapp/templates) | The template catalogue, categories and variables, and sending by name | | [Messages](/docs/guides/whatsapp/messages) | Every message, its status, and its event timeline | | [Events](/docs/guides/whatsapp/events) | The lifecycle events a message emits, and reading them from the API | | [Tracking & metrics](/docs/guides/whatsapp/tracking-and-metrics) | Aggregate delivery and spend numbers (coming soon) | --- title: "Phone number setup" description: "Connecting your own WhatsApp phone number is coming soon. Today Bird provides managed phone numbers and a set of pre-registered templates, and picks the number from the template's category." source: https://bird.com/en-us/docs/guides/whatsapp/phone-number-setup --- # Phone number setup Connecting your own WhatsApp phone number is **coming soon**. For now, Bird provides managed phone numbers and a set of pre-registered templates. Every message goes out from one of Bird's numbers, chosen by the template's category: **authentication** templates send through **Bird Verify**, everything else through **Bird Notify**. A send has no `from` field — see [Sending WhatsApp messages](/docs/guides/whatsapp/sending-whatsapp#category-and-sender). When bring-your-own-number support lands, this page will cover connecting a WhatsApp Business number to your workspace. --- title: "Sending WhatsApp messages" description: "Send a WhatsApp template with POST /v1/whatsapp/messages — the recipient, the template name and language, filling variables through components, the async 202 model, and idempotent retries." source: https://bird.com/en-us/docs/guides/whatsapp/sending-whatsapp --- # Sending WhatsApp messages This guide covers the send endpoint, `POST /v1/whatsapp/messages`. You build one JSON payload with a recipient and a template; Bird returns `202 Accepted` with a message ID and delivers asynchronously. Business-initiated WhatsApp is template-only — you send a pre-approved template, not free text — and each request sends one message to one recipient. There is no batch endpoint. ## A minimal send The smallest valid payload is a `to` recipient and a `template` with its `name`. Include `language` unless the template has a single language, and fill any `{{n}}` variables the template declares through `components`. ```bash curl -X POST https://us1.platform.bird.com/v1/whatsapp/messages \ -H "Authorization: Bearer bk_us1_..." \ -H "Content-Type: application/json" \ -d '{ "to": "+15551234567", "template": { "name": "bird_otp", "language": "en", "components": [ { "type": "body", "parameters": [{ "type": "text", "text": "481920" }] } ] } }' ``` Use your regional host (`https://us1.platform.bird.com` or `https://eu1.platform.bird.com`) with a matching `bk_{region}_...` key. There is no `from` field: Bird selects the sender number from the template's category. ## Building up the payload ### Recipient `to` is a single recipient in [E.164](https://en.wikipedia.org/wiki/E.164) format — a leading `+`, country code, and subscriber number, e.g. `+15551234567`. One message goes to one recipient; there is no recipient array and no batch send, so reach many people with one call per recipient. ### Template `template` names the pre-approved template to send: - **`name`** (required) — the template's name, e.g. `bird_otp`. It must match an approved template in your workspace (lowercase letters, digits, and underscores). - **`language`** — the template's language tag, e.g. `en` or `pt_BR`. Omit it only when the template exists in a single language. - **`components`** — the values that fill the template's `{{n}}` variables (see below). Omit it for a template that has no variables. Browse your approved templates, their languages, and a rendered preview on the [Templates](https://bird.com/dashboard/w/whatsapp/templates) page. ### Components and parameters Templates in the catalogue today carry numbered variables — `{{1}}`, `{{2}}`, and so on. You supply their values through `components`. Each component names a `type` (`header`, `body`, or `button`) and a `parameters` array; each parameter is `{ "type": "text", "text": "..." }`, applied in order (the first parameter fills `{{1}}`). Only `text` parameters are supported today. For example, a one-time-passcode template whose body reads `{{1}} is your verification code` and whose button copies the code takes the code as both a body parameter and a button parameter: ```json { "components": [ { "type": "body", "parameters": [{ "type": "text", "text": "481920" }] }, { "type": "button", "parameters": [{ "type": "text", "text": "481920" }] } ] } ``` ### Category and sender Category and sender are properties of the template, not of the send. A template's category — `authentication`, `utility`, or `marketing` — determines how WhatsApp treats the message, which sender number Bird uses, and, together with the destination country, what the message costs. There is no `from` field on the request. ### Field reference | Field | Type | Required | Limits / notes | | :-------------------- | :------------- | :------- | :----------------------------------------------------------------------------- | | `to` | string (E.164) | yes | One recipient per message | | `template.name` | string | yes | An approved template name; lowercase letters, digits, and underscores | | `template.language` | string | no\* | Template language tag (e.g. `en`, `pt_BR`); omit for single-language templates | | `template.components` | array | no | Fills `{{n}}` variables; component `type` is `header`, `body`, or `button` | \* `language` is optional only when the template has a single language. ## What isn't supported yet The request accepts `to` and `template` and nothing else. The following are part of the roadmap but rejected today — a request that includes them fails with a `422`: - **Free-text (non-template) sends** — WhatsApp sends are template-only in this release. - **Non-text parameters** — media, location, and other parameter types are reserved; only `text` is supported. - **`tags` and `metadata`** — not accepted on a WhatsApp send. - **Batch sending** — there is no `/v1/whatsapp/batches`; send one message per call. - **Recipients without a phone number** — a WhatsApp-ID-only recipient is rejected before any charge. ## The async model: what 202 means A successful send returns `202 Accepted` with a message ID and `status: accepted`. The `202` is returned only after the send is durably accepted — it is never accepted and then silently dropped. Hard failures you can fix (an invalid recipient, an unknown template, a missing variable) fail immediately with a `422`; a send that would exceed your workspace balance, or a route with no price, is rejected before anything is charged. Actual delivery happens asynchronously: the message moves to `sent` when Bird hands it to WhatsApp, then to a terminal status (`delivered` or `failed`) when the receipt arrives, reported through events, [webhooks](/docs/guides/webhooks), and the read endpoints. A read receipt is surfaced separately as a `read_at` timestamp and a `whatsapp.read` event, not as a status. ## Cost and billing > **Billing** — Bird charges for a WhatsApp message when it is **sent**, regardless of whether it is ultimately delivered. The price depends on the destination country and the template's category; see [WhatsApp pricing](https://bird.com/pricing/whatsapp). WhatsApp is priced per message, keyed on the template's category and the recipient's country. Bird charges your wallet up-front, when it accepts and processes the send — before the message is dispatched to WhatsApp — and the charge stands whether or not the message is later delivered. If the route has no price or your balance is insufficient, the send is rejected with an error and nothing is charged. ## Retrying safely Send the `Idempotency-Key` header with a unique value per logical send, and retries become safe: if your first request succeeded but you never saw the response (timeout, dropped connection), replaying the same request with the same key returns the original result — and the response carries an `Idempotency-Replay` header — instead of sending, and charging for, a duplicate message. See [idempotency](/docs/guides/idempotency) for key format and retention. ## Next steps - [WhatsApp overview](/docs/guides/whatsapp/overview) — the channel, the dashboard app, and where everything lives - [Webhooks & events](/docs/guides/webhooks) — receive delivery and read events on your own endpoint - [Idempotency](/docs/guides/idempotency) — safe retries with the `Idempotency-Key` header --- title: "WhatsApp templates" description: "Browse the WhatsApp template catalogue in the dashboard, read a template's category, language, and variables, and send by template reference." source: https://bird.com/en-us/docs/guides/whatsapp/templates --- # WhatsApp templates Business-initiated WhatsApp messages are sent from a **template** — a pre-approved message that WhatsApp has reviewed for its category. A template carries fixed text plus `{{n}}` variables, so a send only supplies the values that change (an OTP code, an order number) and WhatsApp assembles the final message. Today every WhatsApp send names a template; free-text and other message types will follow. Templates are **Bird-managed** today. The WhatsApp MVP is a catalogue of templates Bird has registered with WhatsApp; you send from it rather than authoring your own. The [Templates](https://bird.com/dashboard/w/whatsapp/templates) page shows what's available and exactly how each one renders. ![The WhatsApp Templates page in the Bird dashboard: a search box with Status and Category filters above a table of approved templates (bird_otp, bird_signin_alert, bird_appointment_reminder, bird_delivery_update), each row showing its Status, Name, an en Language, a Category of authentication or utility, and a Bird-managed WABA](/images/docs/dashboard-whatsapp-templates.png) ## Browsing templates in the dashboard The [**Templates**](https://bird.com/dashboard/w/whatsapp/templates) tab, under WhatsApp, lists every template available to your workspace. Search by name and filter by status or category to find one. Each row shows the fields you need to pick and send a template: - **Status** — the template's WhatsApp review state. **Approved** templates are live and sendable; `Pending`, `Rejected`, `Paused`, `Disabled`, `In appeal`, `Pending deletion`, and `Limit exceeded` track a template's standing with WhatsApp. - **Name** — the template reference (for example `bird_otp`). This is what you pass as `template.name` when sending. - **Language** — the language the template is registered in (e.g. `en`, `pt_BR`). - **Category** — `authentication`, `utility`, or `marketing`. The category governs how WhatsApp treats the message, which sender number Bird uses, and, with the destination country, the [price](https://bird.com/pricing/whatsapp). - **WABA** — the WhatsApp Business Account the template belongs to; Bird-managed templates are marked accordingly. Click a row to open the template's detail. ## The template detail The detail view renders the template the way a recipient sees it — a WhatsApp-style message bubble with the body text, any `{{n}}` variables highlighted, and the template's buttons — so you can confirm the wording and layout before you send. Alongside the preview is a ready-to-run **cURL example**: a complete [`POST /v1/whatsapp/messages`](/docs/guides/whatsapp/sending-whatsapp) call for that template, pointed at your region's host, with a `components` array pre-filled with the template's example values. Copy it, swap in your API key and real values, and send. ## Listing templates from the API `GET /v1/whatsapp/templates` returns the catalogue. Reading templates needs an API key with the `whatsapp_management` scope: ```bash curl https://us1.platform.bird.com/v1/whatsapp/templates \ -H "Authorization: Bearer bk_us1_..." ``` Each template carries its `name` (the reference you send by), `language`, `category`, `status`, and its `components` — the `header`, `body`, and `button` parts, each with the example values that show how its `{{n}}` variables are filled. Read a template's components to know which parameters a send must supply. ## Sending with a template Name the template in the send's `template` object and fill its variables through `components` — see [Sending WhatsApp messages](/docs/guides/whatsapp/sending-whatsapp) for the full payload: ```bash curl -X POST https://us1.platform.bird.com/v1/whatsapp/messages \ -H "Authorization: Bearer bk_us1_..." \ -H "Content-Type: application/json" \ -d '{ "to": "+15551234567", "template": { "name": "bird_otp", "language": "en", "components": [ { "type": "body", "parameters": [{ "type": "text", "text": "481920" }] } ] } }' ``` ## Next steps - [Sending WhatsApp messages](/docs/guides/whatsapp/sending-whatsapp) — the full send payload the `template` object slots into - [WhatsApp messages](/docs/guides/whatsapp/messages) — find a sent message and follow its lifecycle - [WhatsApp pricing](https://bird.com/pricing/whatsapp) — how category and destination set the price --- title: "Tracking & metrics" description: "Aggregate WhatsApp metrics — delivery rate, failure rate, volume, and spend — are coming soon. Until then, use the Messages page for per-message visibility." source: https://bird.com/en-us/docs/guides/whatsapp/tracking-and-metrics --- # Tracking & metrics Aggregate WhatsApp metrics — delivery rate, failure rate, accepted volume, and spend over time — are **coming soon**. Until then, you can still see how your WhatsApp channel is doing per message: - **[Messages](/docs/guides/whatsapp/messages)** — every send and receipt with its status and event timeline. Filter by status to see, for example, everything that failed in a date range. - **[Events](/docs/guides/whatsapp/events)** — the per-message lifecycle stream from the events API, if you want to aggregate outcomes yourself. - **[WhatsApp pricing](https://bird.com/pricing/whatsapp)** — what each message costs, by destination country and template category. ## Next steps - [WhatsApp messages](/docs/guides/whatsapp/messages) — the per-message view available today - [Sending WhatsApp messages](/docs/guides/whatsapp/sending-whatsapp) — the send API and its cost model --- title: "Workspaces" description: "The workspace is where you work in Bird: domains, API keys, sends, and team. What's scoped to it, how requests resolve context, regions, and deletion." source: https://bird.com/en-us/docs/guides/workspaces --- # Workspaces The **workspace** is the unit of work in Bird: where domains are verified, API keys are minted, email is sent, and your team has access. The dashboard is workspace-first — the breadcrumb always names your workspace, and everything you do happens inside it. Behind the workspace sits an **organization** — light shared plumbing that holds billing and the member list. Today each customer has one workspace, so the organization rarely needs thinking about. It matters mainly if you add more workspaces later (to separate, say, production from staging): each workspace keeps its own API keys, domains, suppressions, and team, while billing, membership, and IP infrastructure stay shared above them. ## What's scoped where | Level | What lives there | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Organization | [Members and invitations](/docs/guides/users-teams-roles), [billing, plan, and the wallet](/docs/guides/billing-and-usage), dedicated IPs and IP pools, region | | Workspace | [API keys](/docs/guides/authentication), the team and its roles, sending-domain assignments, emails, suppressions, [webhook endpoints](/docs/guides/webhooks) | Two of these placements are worth dwelling on: - **IP pools are organization-owned.** Dedicated IPs and the pools that group them belong to the organization and are shared by all its workspaces — reputation you warm in one workspace is reputation every workspace benefits from. Workspaces get read-only visibility into the pools; purchasing and pool management are organization-level operations. - **Isolation is per workspace.** A workspace's API keys, emails, suppressions, and webhooks are invisible to every other workspace. If you add a second workspace, a key minted in staging can never read production sends, and a suppression added in one does not suppress in another. Shared state lives only at the organization level. ## Regions Every organization is pinned to exactly one region — currently `us1` (United States) or `eu1` (European Union). The region is set when the organization is created and is immutable: all of the organization's workspaces, data, and sending infrastructure live there, and its API traffic goes to that region's host (`https://us1.platform.bird.com`, `https://eu1.platform.bird.com`). The region is baked into every API key prefix (`bk_us1_...`), so requests route themselves — see [Regions](/docs/api/regions). ## How requests resolve their context Bird's API uses flat routes — `/v1/email/messages`, `/v1/members` — with no organization or workspace segment in the path. The context comes from how the request authenticates: - **API keys are workspace-locked.** A key belongs to exactly one workspace, which also determines the organization and region. The context is implicit in the credential; no headers are needed, and a key can never reach another workspace's resources. If a request carries a context header that contradicts the key's workspace, it is rejected with `403` rather than silently reinterpreted. - **Sessions select context explicitly.** A person can belong to many organizations and workspaces, so session-authenticated requests (the dashboard) name their target per request: `X-Organization-Id` on organization-scoped endpoints, `X-Workspace-Id` on workspace-scoped ones. A session without a grant in the named context gets `403`. The model mirrors reality: machines operate on one workspace implicitly, humans pick their context explicitly. Whether the request is then allowed is a separate question, answered by [roles](/docs/guides/users-teams-roles) for sessions and scopes for keys. ## Deletion Deleting a workspace or an organization is deliberately hard to do by accident and deliberately recoverable for a while: - **Lockout is immediate.** The moment a deletion is accepted, access stops: logins are refused, API keys stop authenticating and are revoked, sends are rejected. From the outside, it's gone. - **A 7-day grace window follows.** Data and configuration are retained, untouched, for 7 days. A mistaken deletion can be reversed in this window — contact support to restore it. Everything comes back except API keys, which were revoked at lockout and must be re-issued. - **Then the purge is irreversible.** After the window, cleanup runs asynchronously and reclaims everything — domains, suppressions, webhook endpoints, memberships, data. There is no recovery after the purge begins. Two guardrails apply on the way in: an organization's **last workspace cannot be deleted** (`409` — delete the organization instead if that is what you mean), and deleting an organization requires the owner role. ## Next steps - [Authentication & API keys](/docs/guides/authentication) — how keys bind to a workspace - [Users, teams & roles](/docs/guides/users-teams-roles) — workspace roles, plus the org roles that manage billing - [Regions](/docs/api/regions) — regional hosts and routing --- title: "Knowledge base" description: "Practical help for using Bird — account setup, billing, DNS and domains, deliverability, compliance, and troubleshooting." source: https://bird.com/en-us/docs/knowledge-base --- # Knowledge base The knowledge base is the practical, how-do-I side of Bird — setting up your account and domains, keeping deliverability healthy, and sorting out problems when they come up. Where the [Guides](/docs/guides) explain how to build on the API, these articles help you operate day to day. - **[Getting started](/docs/knowledge-base/getting-started/create-account)** — create your account and workspace, tour the dashboard, and invite your team. - **[Account & security](/docs/knowledge-base/account-security/login-mfa)** — login and MFA, API keys, the audit log, and SSO. - **[Billing](/docs/knowledge-base/billing/plans-and-pricing)** — plans and pricing, usage and invoices, payment methods and the wallet. - **[DNS & domains](/docs/knowledge-base/dns-and-domains/generic-registrar)** — step-by-step DNS setup for every major registrar, and why a domain might not verify. - **[Deliverability](/docs/knowledge-base/deliverability/warming)** — warming, list hygiene, staying out of spam, and the Gmail & Yahoo sender rules. - **[Compliance](/docs/knowledge-base/compliance/consent-and-privacy)** — consent, data privacy, and CAN-SPAM in plain language. - **[Troubleshooting](/docs/knowledge-base/troubleshooting/bounces)** — bounces, rejections, throttling, delays, and common error messages. - **[FAQ](/docs/knowledge-base/faq/email-sending)** and **[Policies](/docs/knowledge-base/policies/acceptable-use)** — quick answers and the rules of the road. --- title: "API key management" description: "Create, scope, and revoke the API keys your applications use to call Bird — and keep the secrets safe." source: https://bird.com/en-us/docs/knowledge-base/account-security/api-keys --- # API key management An API key is a credential for software, not people. It belongs to a single workspace, and any application holding it can call the Bird API as that workspace — send email, read delivery status, whatever the key's permissions allow. You manage keys entirely from the dashboard; this page covers the operational side. For the full technical contract (key format, request headers, scope tables), see [Authentication & API keys](/docs/guides/authentication). ## Where keys live Open [**Developers → API keys**](https://bird.com/dashboard/w/api-keys) in your workspace. The page lists every key with its name, masked prefix, scopes, and when it was last used — useful for spotting keys that are no longer in service. ![The API Keys page in the Bird dashboard, listing keys with their masked prefix, scopes, and last-used time](/images/docs/dashboard-api-keys.png) Creating and revoking keys requires the right workspace role — Admins and Developers have it. See [Users, teams & roles](/docs/guides/users-teams-roles). ## Creating a key 1. On the API keys page, choose to create a new key. 2. **Name it for its job**, not its owner — "Billing service production" beats "Dave's key". Keys outlive the people who create them. 3. **Pick scopes.** Each scope is granted at `read` or `write` level and controls what the key can do — for example, sending email versus only reading delivery status. Grant the narrowest set that works: a key that only sends email shouldn't be able to manage suppressions. 4. Create the key and **copy the secret immediately**. ### The secret is shown once The full key appears exactly once, at creation. Bird stores only a one-way hash — after you leave that screen, nobody (including Bird) can show it to you again. Paste it straight into your secret manager or deployment configuration. If you lose it, you don't recover it; you revoke the key and create a new one. A key's scopes are fixed at creation and can't be edited later. To change what a key can do, create a new key with the right scopes and revoke the old one — that way every key's permissions are exactly what they were when it was issued. ## Revoking a key Revoke a key from its detail page when it's no longer needed, when the person who had access to it leaves, or any time you suspect it leaked. Revocation is permanent — there is no un-revoke — and takes effect almost immediately: within a few minutes at most, every request using that key is rejected. If you suspect a key has leaked, revoke first and ask questions later. Your application goes down for the time it takes to deploy a replacement key; a leaked key in the wrong hands costs far more. To rotate a key without downtime: create a new key with the same scopes, deploy it, watch the old key's last-used time until traffic has moved, then revoke the old one. ## Everything is recorded Key creation and revocation are recorded in your organization's [audit log](/docs/knowledge-base/account-security/audit-log) — who did it, to which key, and when. Revoked keys also stay visible in the key list's history, so the record of what existed never disappears. ## Good habits - **One key per application or environment.** Separate keys for staging and production mean revoking one doesn't take down the other, and the audit trail tells you which system did what. - **Watch the last-used column.** A key that hasn't been used in months is risk with no benefit — revoke it. - **Never put keys in client-side code or public repositories.** Keys belong in server-side configuration and secret managers only. ## Next steps - [Authentication & API keys](/docs/guides/authentication) — key anatomy, scope details, and the API contract - [Audit log](/docs/knowledge-base/account-security/audit-log) — reviewing key creation and revocation events - [Users, teams & roles](/docs/guides/users-teams-roles) — who can manage keys --- title: "Audit log" description: "A chronological record of who changed what in your organization — and how to use it for security reviews and incident follow-up." source: https://bird.com/en-us/docs/knowledge-base/account-security/audit-log --- # Audit log The audit log is your organization's change history: a chronological, read-only record of every management action taken in Bird. When you need to answer "who changed what, and when?" — after a surprise configuration change, during a security review, or while following up on an incident — this is where you look. You'll find it on the [**Audit**](https://bird.com/dashboard/w/logs/audit) tab of the dashboard's **Logs** page: one organization-wide record, viewable from any of your workspaces, with a filter to narrow it to a single workspace. ![The audit log in the Bird dashboard: a table of management actions — Created API key, Created suppression, Rotated webhook secret, Created organization dedicated IP, Created inbound address — each with its time, the actor's email, the workspace it applied to, and a green Succeeded outcome badge, above search and filter controls and an OCSF export button](/images/docs/dashboard-logs-audit.png) ## What gets recorded The audit log captures management changes — actions that change how your account is set up, not the messages you send. That includes: - **API keys** — creation and revocation - **Sending domains** — adding, verifying, and deleting domains - **Webhooks** — creating, updating, and deleting endpoints, and rotating signing secrets - **Workspaces and organization** — creating, renaming, and deleting workspaces; organization setting changes - **Team** — invitations, role changes, and member removals - **Security events** — logins (including failed attempts), MFA changes, recovery-code regeneration, and password resets - **Billing actions** — changes made through your billing settings Day-to-day traffic is deliberately not in the audit log: individual emails sent, delivery and open events, and read-only views of pages don't generate entries. Those live in your messaging logs and analytics instead. ## Reading an entry Every entry answers the same three questions: - **Actor — who.** The user who clicked, or the API key that made the call. Actions taken by Bird's own systems are marked as such. - **Action — what.** A short name like `api_key.created`, `domain.verified`, or `member.role_updated`: the resource that changed and what happened to it. - **Timestamp — when.** The exact time the change happened. Entries also carry context — the target resource, the IP address and client the request came from, and a request ID — so you can tie an entry back to a specific machine or support case. Sensitive values such as key secrets never appear in entries. ## Using it in practice - **"Why did sending break on Tuesday?"** Filter to that day and look for domain, webhook, or API key changes just before the breakage. - **Security review.** Skim security events for failed logins you don't recognize, MFA factors being removed, or API keys created outside normal change windows. - **Offboarding check.** After someone leaves, confirm their member removal is recorded and review what they changed in their final weeks. - **Incident follow-up.** The actor, IP, and request ID on each entry give you a concrete trail to include in an incident write-up. ## Who can see it Audit log access is an organization-level permission, typically held by organization admins and owners. Members without it won't see the page. Entries cannot be edited or deleted by anyone — including admins — which is what makes the log trustworthy as a record. ## Next steps - [API key management](/docs/knowledge-base/account-security/api-keys) — the key events you'll see most often - [Login, password & MFA](/docs/knowledge-base/account-security/login-mfa) — the security events behind the log entries --- title: "Login, password & MFA" description: "Keep your Bird account secure — set a strong password, enable two-factor authentication, and review where you're signed in." source: https://bird.com/en-us/docs/knowledge-base/account-security/login-mfa --- # Login, password & MFA Your Bird account is protected by your password, optional multi-factor authentication (MFA), and session controls you manage yourself. This page covers the everyday tasks: changing your password, turning on MFA, and checking where you're signed in. For the technical contract behind API access, see [Authentication & API keys](/docs/guides/authentication). ## Signing in You sign in to the Bird dashboard with your email address and password, or with a connected Google or GitHub account. If MFA is enabled on your account, you'll be asked for a second factor — a code from your authenticator app, a code sent by SMS, or a recovery code — before the session starts. ## Your password Passwords must be at least 12 characters. There's no maximum length and no arbitrary complexity rules — a long passphrase works fine. Bird also checks new passwords against known data-breach lists and rejects ones that have appeared in a breach, so if a password is refused as compromised, it has been exposed somewhere before; pick a different one. - **Change your password** under your profile's [**Security**](https://bird.com/dashboard/profile/security) settings in the dashboard. You'll need your current password. - **Forgot it?** Use the **Forgot password** link on the login page. Bird emails you a reset link that's valid for a short window and works only once. Resetting your password signs you out everywhere: all of your active sessions are invalidated, so anyone holding an old session — including someone who shouldn't be — has to log in again with the new password. ## Multi-factor authentication (MFA) MFA adds a second check at login so a stolen password alone isn't enough to get into your account. Bird supports two factor types: - **Authenticator app (TOTP)** — six-digit codes from any standard authenticator app (Google Authenticator, 1Password, Authy, and similar). This is the recommended option. - **SMS codes** — a six-digit code sent to your phone. Available in supported countries. ### Enabling an authenticator app 1. Open your profile's Security settings in the dashboard. 2. Choose to add an authenticator app. Bird shows a QR code (and the secret as text, if you prefer to type it). 3. Scan the QR code with your authenticator app, then enter the six-digit code it generates to confirm. When you enable MFA for the first time, Bird gives you a set of **recovery codes**. Save them somewhere safe — a password manager or printed copy. Each code works once and gets you into your account if you lose your phone. You can regenerate the set from the same Security settings at any time; regenerating invalidates all previous codes. ### Removing a factor You can remove an enrolled factor from the same Security settings. If you remove your last verified factor, MFA is switched off for your account entirely — logins go back to password only, so only do this deliberately. ## Social login (Google & GitHub) You can sign up for and sign in to Bird with a Google or GitHub account instead of a password. If a social account's verified email matches an existing Bird account, signing in links the two — and Bird emails the account owner so an unexpected link doesn't go unnoticed. You can also connect or disconnect Google and GitHub from your existing account under your profile's Security settings. ## Active sessions Your profile's [**Sessions**](https://bird.com/dashboard/profile/sessions) page in the dashboard lists everywhere you're currently signed in — each session with its device/browser details, and your current session marked. If you see a session you don't recognize, sign it out from that page; you can also sign out all other sessions in one click. A signed-out session is invalid immediately. Sessions also expire on their own: after roughly two days without activity, and after two weeks at the longest regardless of activity. ## Security notification emails Bird emails you when something security-relevant changes on your account, so unexpected activity is visible even if you weren't the one acting: - **Password changed** — sent after a password change or reset. - **MFA enabled** — sent when two-factor authentication is first enabled on your account. - **Social account linked** — sent when a Google or GitHub identity is connected to your account. If you receive one of these and didn't make the change, reset your password immediately and review your [active sessions](#active-sessions) and the [audit log](/docs/knowledge-base/account-security/audit-log). ## Next steps - [API key management](/docs/knowledge-base/account-security/api-keys) — credentials for apps, not people - [Audit log](/docs/knowledge-base/account-security/audit-log) — who changed what, when - [Authentication & API keys](/docs/guides/authentication) — the developer reference --- title: "SSO & provisioning" description: "Connect your company's identity provider so your team signs in to Bird through your own login — with provisioning and enforcement handled centrally." source: https://bird.com/en-us/docs/knowledge-base/account-security/sso --- # SSO & provisioning Single sign-on (SSO) lets the members of your organization sign in to Bird through your company's own identity provider — Okta, Microsoft Entra ID, Google Workspace, or any other system that speaks SAML 2.0 or OIDC — instead of managing a separate Bird password. SSO is an enterprise capability and is set up per organization; it is being rolled out and is not yet self-serve in the dashboard, so talk to your Bird account team to get it enabled for your organization. This is different from signing in with a personal Google or GitHub account, which any individual user can do — see [Login, password & MFA](/docs/knowledge-base/account-security/login-mfa). SSO is configured once by an organization admin and applies to everyone whose email belongs to your company's domain. ## How it works, in plain terms Your organization connects its identity provider (IdP) to Bird, covering one or more verified email domains (say, `yourcompany.com`). Once the connection is active, anyone with an email address on those domains can sign in to Bird by authenticating with your company login — the same screen they use for everything else at work. Bird links each person to a stable identity from your IdP, not to their email address, so a name or email change on your side doesn't break their Bird account. ## What setup involves Setup is done by an organization admin working with your identity-provider admin (often the same team): 1. **Prove you own the domain.** Bird gives you a DNS record to publish for each email domain the connection will cover. Enforcement can't be turned on until the domain is verified. 2. **Exchange configuration with your IdP.** Your IdP admin creates a Bird app in your identity provider and provides the usual details — for SAML, the metadata or sign-in URL and signing certificate; for OIDC, the issuer and client credentials. Bird provides its side (the service-provider details) for your IdP admin to paste in. 3. **Test before turning it on.** Connections support a test mode, so you can verify a sign-in end to end before any member is affected. 4. **Choose an enforcement level.** SSO can be optional (members may use SSO or their password) or required (members on the covered domains must use SSO). Even with SSO required, organization owners can always sign in with password and MFA — a deliberate break-glass path so an identity-provider outage never locks you out of your own organization. ## What changes for your team Once SSO is on, members on the covered domains pick the SSO option at the Bird login and are sent through your company login. New employees can be provisioned just in time: the first time they sign in through your IdP, a Bird account is created for them and joined to your organization — no invitation email needed. What they can do inside Bird is still governed by their Bird role; see [Users, teams & roles](/docs/guides/users-teams-roles). Day to day, that means joiners get access through your IdP, and leavers lose access when you disable them there — your identity provider becomes the front door. Automatic directory sync (SCIM) — pushing user creation, updates, and deactivation from your IdP into Bird without anyone signing in — is planned as a follow-on and isn't part of the initial SSO offering. ## Next steps - [Login, password & MFA](/docs/knowledge-base/account-security/login-mfa) — individual account security, including personal Google/GitHub sign-in - [Users, teams & roles](/docs/guides/users-teams-roles) — how roles and permissions work once members are in - [Authentication & API keys](/docs/guides/authentication) — the developer-facing authentication reference --- title: "Payment methods & wallet" description: "Add and update payment methods, understand how your prepaid wallet works, top it up, and keep your balance from interrupting sending." source: https://bird.com/en-us/docs/knowledge-base/billing/payment-methods-wallet --- # Payment methods & wallet How you pay Bird comes down to two things: a **payment method** (typically a card) and your **wallet** — a shared prepaid balance that charges are drawn from. This page covers managing both, so payments go through smoothly and sending is never interrupted by a billing hiccup. Everything here lives in the dashboard: open **Billing and payments**. It's managed by organization owners and billing admins. ## Payment methods Open the **Payment Methods** tab to add a card or update the one on file. A few practical notes: - **Keep at least one valid method on file.** Plan charges and wallet top-ups are made against it, so an expired card is the most common cause of a failed payment. - **Updating a card** replaces it for future charges — past invoices and transactions are unaffected. - **If a payment fails**, it isn't a dead end: failed payments are retried and handled according to your plan's terms, and your billing notification addresses are informed so someone can fix the card before it matters. To check or set those addresses, open **Billing and payments** — point them at a team alias, not one person's inbox. ## The wallet You have one wallet — a prepaid balance shared across your workspaces. Charges flow out of it, and top-ups flow into it. There's never a question of which workspace's balance pays for what: it's all one pot. You can see the current balance any time in the **Wallet** tab. ## Topping up To add funds, open the **Wallet** tab and start a top-up: choose an amount, confirm the payment method, and submit. A successful top-up looks like this: 1. The payment is charged to your payment method. 2. Your wallet balance increases by the top-up amount — you'll see the new balance in the **Wallet** tab right away. 3. The top-up appears as a credit in the **Transaction History** tab, alongside every other charge and credit on the wallet. That **Transaction History** tab is the full ledger of your wallet — every top-up, plan charge, and usage charge in order — so it's the place to reconcile any balance question. ## Auto top-up Where available, you can configure **auto top-up** in the **Wallet** tab: pick a threshold and a top-up amount, and when your balance drops below the threshold, Bird automatically tops it up from your payment method. If you send continuously, this is the simplest way to make sure sending is never interrupted by a balance that quietly ran dry — no one has to remember to top up manually. Automatic top-ups appear in the **Transaction History** tab like any other top-up, so the ledger stays complete. ## Stay ahead of a low balance Two habits keep billing from ever becoming an operational problem: - **Set billing notification emails.** Your organization's billing notification addresses receive low-balance warnings before the wallet runs out, along with invoice notifications and plan-change confirmations. A warning that arrives at a team alias gets acted on; one that lands in a departed employee's inbox does not. - **Glance at Transaction History when something looks off.** Every movement of money is in that ledger, timestamped — between it and the [Invoices tab](/docs/knowledge-base/billing/usage-and-invoices), you can account for every cent. ## Next steps - [Usage & invoices](/docs/knowledge-base/billing/usage-and-invoices) — what you're paying for, and where the invoices live. - Developers can read the wallet and transaction ledger programmatically — see [Billing & usage](/docs/guides/billing-and-usage). --- title: "Plans & pricing" description: "Compare Bird's plan tiers — what each one includes, how usage beyond your plan is charged, and how to view or change your plan." source: https://bird.com/en-us/docs/knowledge-base/billing/plans-and-pricing --- # Plans & pricing Your plan, wallet, and invoices are shared across your workspaces. This page explains what separates the plan tiers, how charges beyond your plan work, and how to see or change the plan you're on. ## What's in a plan Every plan has three parts: - **A recurring charge.** A fixed amount billed each billing period, which covers the plan itself and everything included in it. - **Included usage.** Each plan includes a monthly allotment of email sends. As long as you stay inside it, sending costs nothing extra — it's already part of your plan charge. - **Tier-based capabilities.** Higher tiers raise the operational ceilings: how fast you can send, and how many workspaces, sending domains, and dedicated IPs you can hold. For the current allowances and prices on each plan, open **Billing and payments** and the **Subscriptions** tab — or see [the pricing page](/pricing). Those are always the source of truth, and this article won't repeat numbers that may change. ## The plan tiers Plans come in four tiers, and the right one mostly depends on how much you send and how much room you need to grow: - **Free** — for trying Bird and low-volume sending. A small monthly allowance with a daily sending cap, and no credit card required to start. A single sending domain; dedicated IPs aren't available. - **Startup** — for getting going. More included volume and room for several sending domains, with sensible ceilings on sending speed and workspaces. Dedicated IPs aren't available at this tier. - **Growth** — for teams sending at real volume. A larger included allotment, meaningfully higher sending-speed ceilings, more headroom on workspaces and domains, and dedicated IPs available as a paid add-on. - **Custom** — for high-volume senders. The highest limits, plus a direct relationship with Bird for support and custom arrangements. Talk to sales to set one up. Limits on Bird are conversations, not walls: if you're approaching a ceiling on your current tier, contact support — per-organization adjustments above the standard tier values are a normal part of scaling. ## When you go over: overage and pay-as-you-go Two kinds of charges can appear on top of your recurring plan charge: - **Overage.** If you send more email in a billing period than your plan includes, the extra sends are metered and charged at your plan's overage rate, and the overage appears on your next invoice. - **Pay-as-you-go items.** Some products aren't part of a plan allotment at all; they're billed per use, at a published rate, only when you use them. They show up as their own line items. You can watch where you stand at any time — see [Usage & invoices](/docs/knowledge-base/billing/usage-and-invoices). ## See your current plan Open **Billing and payments** and the **Subscriptions** tab. You'll see your current plan and what it includes. To watch how much of the included volume you've used so far this period, open the **Usage** tab. Billing is managed by organization owners and billing admins — if you don't see the billing settings, ask one of them. ## Change or upgrade your plan On the **Subscriptions** tab, click **Upgrade plan** to compare plans and pick a different one. A few things to know: - **Plan changes take effect per the billing cycle.** Upgrades and downgrades are applied in line with your billing period rather than mid-stream, so you won't see partial-period surprises — the confirmation screen tells you exactly when the new plan starts. - **You'll get a confirmation email.** Plan-change confirmations go to your billing notification addresses, so the people who watch the bill know what changed. - **Make sure your payment setup is ready.** A higher plan means a higher recurring charge, so check that your payment method and wallet are in order — see [Payment methods & wallet](/docs/knowledge-base/billing/payment-methods-wallet). ## Next steps - [Usage & invoices](/docs/knowledge-base/billing/usage-and-invoices) — watch your consumption against the plan and read your invoices. - [Payment methods & wallet](/docs/knowledge-base/billing/payment-methods-wallet) — how you actually pay. - Developers on your team can read the technical side in [Billing & usage](/docs/guides/billing-and-usage). --- title: "Usage & invoices" description: "Monitor your usage against plan allowances, understand what each invoice line item means, and view or export your billing history." source: https://bird.com/en-us/docs/knowledge-base/billing/usage-and-invoices --- # Usage & invoices Everything you consume — emails sent, pay-as-you-go products used — is metered against one billing period and shows up on one invoice stream. This page shows where to watch your usage so the bill is never a surprise, and how to read the invoice when it arrives. ## How usage adds up Usage is shared across your workspaces: they all draw from the same plan allotment, and their sends count together against your included volume. Each billing period starts the count fresh. Within a period: - Sends inside your plan's included volume cost nothing extra — they're covered by the recurring plan charge. - Sends beyond the included volume are counted as **overage** and charged at your plan's overage rate on the period's invoice. - Pay-as-you-go products are charged per use as you go, and the period's total appears on the same invoice. ## Watch your usage Open **Billing and payments** and the **Usage** tab. For the current billing period you'll see, per product: - **How much you've used** against how much your plan includes, with a progress bar so you can see utilization at a glance. - **Any overage so far** — if you've gone past the included volume, the overage amount is called out. - **Pay-as-you-go spend** — for per-use products, the number of uses and what they've cost so far this period. If you're regularly running into overage, that's usually the signal to look at the next plan tier — see [Plans & pricing](/docs/knowledge-base/billing/plans-and-pricing). And to make sure the right people hear about billing events without checking the dashboard, set your billing notification addresses (ideally a team alias, not one person's inbox) — they receive invoice notifications, low-balance warnings, and plan-change confirmations. ## Find your invoices Open **Billing and payments** and the **Invoices** tab. Each billing period produces one invoice. From the list you can open any invoice to see its line items, and download it for your records or your finance team — your full billing history stays available there. For the money-movement view — every charge and top-up as it happened — the **Transaction History** tab shows the running ledger of your wallet, which is useful for reconciling payments against invoices. See [Payment methods & wallet](/docs/knowledge-base/billing/payment-methods-wallet). ## Reading an invoice An invoice has up to three kinds of line items, and they map directly to how plans work: - **The recurring plan charge.** The fixed amount for your plan that period. It covers your included usage — this is the line you'll see every period regardless of how much you sent. - **Metered overage.** Only appears if you sent more than your plan included. It shows the overage quantity and what it cost at your plan's overage rate. - **Pay-as-you-go items.** One line per per-use product you used that period, showing the usage and the resulting charge. If a line item looks unexpected, the **Usage** tab for that period is the place to cross-check — the invoice reflects exactly the metering shown there. ## Next steps - [Plans & pricing](/docs/knowledge-base/billing/plans-and-pricing) — what each tier includes and how to change plans. - [Payment methods & wallet](/docs/knowledge-base/billing/payment-methods-wallet) — how invoices get paid and how the prepaid balance works. - Developers can pull the same usage and invoice data programmatically — see [Billing & usage](/docs/guides/billing-and-usage). --- title: "Consent & data privacy (GDPR/CCPA)" description: "How consent, GDPR and CCPA obligations, data residency, and data-subject requests work when you send messages through Bird." source: https://bird.com/en-us/docs/knowledge-base/compliance/consent-and-privacy --- # Consent & data privacy (GDPR/CCPA) This article explains the privacy and consent concepts that apply to every channel you send on through Bird: what consent means, what GDPR and CCPA expect of you, where your data physically lives, and how to handle requests from the people you message. It is guidance to help you understand your obligations — not legal advice, and not the contract. The terms that actually govern how Bird processes your data are in Bird's [legal agreements](https://bird.com/legal) (including the Data Processing Agreement and privacy policy); where this article and those differ, the agreements win. For requirements specific to your business, talk to your own legal counsel. Channel-specific rules (like CAN-SPAM for email) build on top of these basics. See [Email · list consent & CAN-SPAM](/docs/knowledge-base/compliance/email/consent) for the email-specific article. ## Who is responsible for what Privacy law splits responsibility between two roles, and it helps to be clear about which one is yours: - **You are the controller.** You decide who to message, why, and what data about them to keep. The people you message are your customers, and the legal obligations to them — collecting consent, honoring opt-outs, answering privacy requests — are yours. - **Bird is the processor.** Bird stores and processes contact data on your instructions to deliver your messages. Bird does not message your contacts on its own behalf or use their data for its own purposes. In practice this means privacy requests from your recipients go to **you**, and you use Bird's tools and APIs to carry them out. ## Consent: get it, record it, honor it Whatever channel you use, the same three habits keep you on the right side of both the law and your recipients: 1. **Get consent before you message.** The person should have taken a clear action that signals they want to hear from you — checking a box, submitting a signup form, replying to a keyword. Pre-ticked boxes and silence do not count as consent under GDPR, and buying or scraping contact lists means you never had consent at all. 2. **Record the consent.** Keep a record of _who_ consented, _when_, _how_ (which form, which page, which checkbox text), and _to what_. If a recipient or a regulator ever asks "why are you messaging me?", that record is your answer. Bird does not capture this for you — consent happens in your product, so the record lives in your systems. 3. **Honor withdrawals immediately.** Consent can be taken back at any time, and once it is, marketing messages must stop. On email, Bird automates a large part of this: unsubscribes and spam complaints automatically suppress the address for broadcast mail (see the [email consent article](/docs/knowledge-base/compliance/email/consent)). ## GDPR and CCPA at a glance The two regimes overlap heavily for messaging. The short version of what each expects: **GDPR** (EU/EEA recipients) requires a lawful basis for processing personal data — for marketing messages that basis is almost always consent. It gives individuals rights to access, correct, export, and erase their data, and to object to processing. Requests must generally be answered within one month. **CCPA/CPRA** (California recipients) gives consumers the right to know what personal information you collect, to delete it, and to opt out of its sale or sharing. It is more opt-out-shaped than GDPR's opt-in model, but the operational work — finding a person's data and deleting it on request — is the same. If you message people in both regions, the practical approach is to build to the stricter standard (opt-in consent, full deletion support) and apply it everywhere. ## Data residency: your data stays in your region Every Bird organization is pinned to a single region at signup — `us1` (United States) or `eu1` (European Union) — and all of the organization's workspaces, messages, contact data, and event logs are stored and processed in that one region. Data is never replicated across regions, which is what makes residency commitments provable: an EU organization's data lives in the EU, full stop. If your privacy posture requires EU data residency (commonly the case for GDPR-driven processing), choose `eu1` at signup. The assignment is immutable, so this is a decision to make up front. See [Base URLs & regions](/docs/api/regions) for how the region model works at the API level. ## Handling data-subject and deletion requests When a recipient exercises a privacy right — most commonly the right to erasure — you are the one who answers, and you carry the request out across every system that holds their data, including Bird: - **Access and export requests.** Use the dashboard or the APIs to retrieve what Bird holds about an address: message history, event logs, and any suppression records. - **Deletion requests.** Remove the contact's data from each Bird surface that holds it. One step that is easy to miss: **suppression records contain the email address and are your responsibility to erase too.** Deleting a suppression is a hard delete — Bird retains nothing afterward — which is exactly what an erasure request needs, but it also means the address becomes sendable again. After honoring an erasure request, make sure your own systems do not re-import or re-message the contact. The mechanics are in the [suppressions developer guide](/docs/guides/email/suppressions). A reasonable internal checklist for an erasure request: stop active sends to the contact, delete them from your contact lists, delete their suppression records in Bird, delete them from your own databases, and record that the request was completed and when. ## Per-channel compliance articles Each channel adds its own rules on top of these basics: - [Email · list consent & CAN-SPAM](/docs/knowledge-base/compliance/email/consent) — CAN-SPAM requirements, valid list consent, and unsubscribe obligations. - [Email · sender vetting & domain registration](/docs/knowledge-base/compliance/email/sender-vetting) — what happens after you add a sending domain. --- title: "Email · list consent & CAN-SPAM" description: "What CAN-SPAM requires of your email, what valid list consent looks like, and how Bird handles unsubscribes and one-click List-Unsubscribe headers." source: https://bird.com/en-us/docs/knowledge-base/compliance/email/consent --- # Email · list consent & CAN-SPAM This article covers the email-specific layer of compliance: what CAN-SPAM requires of every commercial message, what valid consent for an email list looks like, and how unsubscribes work on Bird. It builds on the channel-agnostic [consent & data privacy](/docs/knowledge-base/compliance/consent-and-privacy) article. As always, this is guidance, not legal advice — check requirements for your specific situation with your own counsel. Meeting these rules is not just about the law: Gmail and Yahoo enforce overlapping requirements (one-click unsubscribe, low complaint rates) as a condition of inbox delivery. See [Gmail & Yahoo sender requirements](/docs/knowledge-base/deliverability/gmail-yahoo-requirements). ## What CAN-SPAM requires CAN-SPAM is the US law covering commercial email. Its core requirements are straightforward, and most of them are good practice everywhere, not just for US recipients: - **Accurate From and subject.** The From name, From address, and subject line must honestly identify who is sending and what the message is about. No misleading sender names, no bait-and-switch subjects. - **A physical postal address in marketing mail.** Every commercial message must include your valid physical postal address — a street address, PO box, or registered commercial mail receiving agency. Put it in the footer of every marketing template. - **A clear way to opt out.** Every commercial message needs a conspicuous unsubscribe mechanism that works without a login or a fee. - **Honor opt-outs promptly.** CAN-SPAM allows up to 10 business days to process an opt-out, but on Bird there is no reason to use any of them — unsubscribes take effect immediately (see below). CAN-SPAM applies to _commercial_ mail. Genuinely transactional messages — receipts, password resets, account notices — are exempt from the postal-address and opt-out requirements, but the accurate-header rules apply to everything. ## What valid list consent looks like CAN-SPAM technically permits opt-out marketing, but GDPR does not, mailbox providers punish unconsented mail with spam-folder placement, and your complaint rate is the single biggest threat to your deliverability. Build your list on real consent: - **Express consent** is a clear affirmative action: the person checked an unticked box, submitted a dedicated signup form, or otherwise actively asked for your email. This is the standard GDPR requires for marketing and the one to aim for everywhere. - **Implied consent** — an existing customer relationship without an explicit marketing opt-in — is weaker. Some jurisdictions accept it in narrow circumstances; others do not. If you rely on it, keep the mail closely related to the existing relationship and make opting out effortless. - **Never** buy, rent, or scrape lists. Nobody on a purchased list consented to hear from _you_, and such lists are dense with spam traps and dead addresses that will wreck your sender reputation. **Keep records.** For each subscriber, store when and where they signed up, what the signup form said, and ideally the IP address and a confirmation timestamp. Double opt-in (a confirmation click before the first marketing send) gives you the strongest record and the cleanest list. ## Unsubscribes on Bird Bird handles the mechanical side of opt-outs for you: - **One-click List-Unsubscribe headers.** Bird adds RFC 8058 one-click unsubscribe headers (`List-Unsubscribe` and `List-Unsubscribe-Post`) to marketing mail automatically. This is what powers the native unsubscribe button in Gmail, Yahoo, and other clients — and it is a hard requirement for bulk senders at Gmail and Yahoo. - **Unsubscribes take effect immediately.** When a recipient unsubscribes — via the header or an unsubscribe link — Bird records an `email.unsubscribed` event and automatically adds the address to your workspace [suppression list](/docs/guides/email/suppressions) with `reason: unsubscribe`. The next marketing send to that address is rejected before it leaves the platform. - **Complaints are treated the same way.** A spam complaint (`email.complained`) auto-suppresses the address with `reason: complaint`. Someone who marks your mail as spam has opted out, just less politely. You still own the content side: include a visible unsubscribe link in your marketing templates (the header alone is not enough for CAN-SPAM's "conspicuous" requirement), and mirror suppressions into your own marketing database by subscribing to the `email_suppression.created` webhook. ## Why transactional mail still gets through Unsubscribe and complaint suppressions block only **non-transactional** mail. A recipient who unsubscribed from your newsletter still gets password resets, receipts, and security alerts — those are messages their own actions require, and stopping them would break your product for them. Bird implements this with [categories](/docs/guides/email/categories): sends in the `marketing` category are blocked by unsubscribe and complaint suppressions, while sends in the `transactional` category deliver through them. Because a send defaults to `marketing`, marketing mail respects unsubscribe and complaint suppressions out of the box, with no special handling. Be deliberate about the reverse instead: set `category: "transactional"` only for genuinely operational mail (receipts, password resets), since that is what lets a message deliver through an unsubscribe. Marking marketing content `transactional` to force it through would mean mailing people who opted out, both a deliverability risk and, in many jurisdictions, a legal one. ## Next steps - [Consent & data privacy (GDPR/CCPA)](/docs/knowledge-base/compliance/consent-and-privacy) — the channel-agnostic consent and privacy basics. - [Gmail & Yahoo sender requirements](/docs/knowledge-base/deliverability/gmail-yahoo-requirements) — the mailbox-provider rules that overlap with CAN-SPAM. - [Suppressions](/docs/guides/email/suppressions) — the developer guide to the suppression list and its API. - [Categories](/docs/guides/email/categories) — `marketing` vs `transactional` and how the category drives suppression policy. --- title: "Email · sender vetting & domain registration" description: "What happens after you add a sending domain — how Bird checks your DNS, when the domain becomes verified, and why a verified domain can later flip back." source: https://bird.com/en-us/docs/knowledge-base/compliance/email/sender-vetting --- # Email · sender vetting & domain registration Before Bird delivers mail from your domain, the domain has to be registered in your workspace and verified — proof that you control it and that mailbox providers can authenticate your mail. This article explains what happens behind the scenes after you click "add domain", so you know what to expect and when. For the full setup walkthrough and API details, see the [sending domains developer guide](/docs/guides/email/sending-domains). ## What happens when you add a domain Adding a sending domain — in the dashboard or via the API — registers it against Bird's sending infrastructure and assigns your organization its own DKIM key. The domain starts in `pending` status, and Bird gives you the exact DNS records to publish: - A **DKIM** TXT record — proves ownership and signs your mail. - A **return-path** CNAME — routes bounces back to Bird and covers SPF. - A **DMARC** policy — any valid `v=DMARC1` record; a minimal `p=none` policy is enough. - An optional **tracking** CNAME for branded open/click tracking (not required for sending). There is no manual approval queue for standard domains: verification is entirely automatic, driven by your DNS. The only human-review path is the rare `rejected` status, applied when a domain is refused for policy reasons — if you see it, contact support. ## How verification runs You never have to poll or click a "check now" button. Bird starts checking your DNS records within seconds of registration, then re-checks on a backoff schedule over roughly the first 72 hours, after which the domain folds into a daily sweep that re-checks every active domain. The moment all the required records resolve correctly, the domain flips from `pending` to `verified` and sending unlocks once DKIM, the return-path CNAME, and DMARC are all in place. If you have just fixed a DNS record and want an immediate re-check rather than waiting for the next scheduled one, the verify endpoint (and the equivalent dashboard button) triggers one on demand — rate-limited to 5 calls per domain per hour. ![A domain's DNS Records page in the Bird dashboard, showing the verified DKIM record with copyable name and value, and the return-path and DMARC sections below](/images/docs/dashboard-email-domain-detail.png) ## How long it takes For most domains, verification completes **within minutes** of the DNS records being published correctly — the long pole is your DNS provider's propagation, not Bird. Some registrars propagate in seconds (Cloudflare), others can take an hour or more. If your domain is still `pending` after a few hours, the records are almost certainly not resolving as expected — a typo in the host, a duplicated domain suffix, or a proxied CNAME are the usual suspects. Work through [Why isn't my domain verifying?](/docs/knowledge-base/dns-and-domains/verification-delays) to diagnose it. ## Verified is not forever Verification is continuous, not a one-time gate. The daily sweep keeps checking verified domains, so if your DNS later breaks — a record deleted during a provider migration, a zone change that drops the DKIM TXT — Bird notices and the domain can flip back to unverified, stopping sends until the records are restored. To avoid flapping on transient DNS blips, a verified domain is only downgraded after **two consecutive failed re-checks**, and any successful check resets the counter. Once you fix the records, the domain re-verifies automatically — no need to re-register or contact support. The practical advice: treat your sending-domain DNS records as production infrastructure. If you migrate DNS providers or restructure your zone, carry the DKIM, return-path, and DMARC records over first. ## Next steps - [Sending domains](/docs/guides/email/sending-domains) — the full developer guide: registration API, DNS record details, and the verification lifecycle. - [Why isn't my domain verifying?](/docs/knowledge-base/dns-and-domains/verification-delays) — diagnosing pending domains and DNS propagation. --- title: "Apple Mail Privacy & open tracking" description: "Why open rates look inflated since Apple Mail Privacy Protection, how Bird separates genuine opens from machine prefetches, and which signals to trust." source: https://bird.com/en-us/docs/knowledge-base/deliverability/apple-mail-privacy --- # Apple Mail Privacy & open tracking If your open rates look surprisingly high — or jumped at some point without any change on your side — you're seeing the effect of mail privacy features, most prominently Apple Mail Privacy Protection. Understanding what an "open" actually measures now is essential to reading your metrics honestly. ## What changed Open tracking works by embedding a tiny invisible image in each message; when the recipient's mail client loads the image, an open is recorded. Apple Mail Privacy Protection breaks the link between that image load and a human reading the message: for recipients who use it (most Apple Mail users do, since it's on by default), Apple's servers pre-fetch the message's images on the recipient's behalf — often shortly after delivery, whether or not the message is ever read. Each prefetch registers as an open. Gmail's image proxy adds similar machine-driven noise. The result: a meaningful share of recorded opens are generated by machines, not people. Raw open counts overstate real engagement, and the gap varies with how many of your recipients use Apple Mail — so two audiences with identical real engagement can show very different raw open rates. ## Genuine opens vs machine opens The useful distinction is between **non-prefetched opens** — the image was loaded in a way consistent with a human opening the message — and **machine opens**, where a privacy proxy fetched it automatically. Bird detects prefetches and keeps the two apart: each open event carries an `is_prefetched` flag, and the open rate shown in the dashboard is computed from non-prefetched opens only, so privacy-proxy noise doesn't inflate the headline number. The raw counts remain available alongside the filtered ones for anyone who wants both views — the [tracking and metrics guide](/docs/guides/email/tracking-and-metrics) defines exactly how each figure is computed. This filtering makes Bird's open rate a more honest number than a raw count, but it doesn't make opens a precise measurement. Detection is conservative, not perfect — and the inverse problem also exists: recipients with images blocked or text-only clients read your mail without ever registering an open. ## How to read engagement now Treat opens as a **soft signal**: good for spotting trends and large relative differences, unreliable as an absolute measure or for per-recipient decisions. - **Lean on clicks, replies, and conversions.** A click requires a deliberate human action, which makes it the more trustworthy engagement signal — and replies and downstream conversions are stronger still. When you compare campaigns or judge whether content lands, weight these over opens. - **Compare like with like.** Open rates are still useful relatively — this month vs last month, variant A vs variant B — as long as the audience mix is similar, since the same share of machine noise affects both sides. - **Don't prune your list on opens alone.** Deciding someone is "inactive" because they never registered an open misclassifies text-only and image-blocking readers. Use absence of clicks over a long window as the pruning signal instead. ## Next steps - [Tracking & metrics](/docs/guides/email/tracking-and-metrics) — how open and click tracking are instrumented and how the rates are computed - [Events and webhooks](/docs/guides/email/events) — the per-recipient event definitions, including the `is_prefetched` flag on open events --- title: "Gmail & Yahoo sender requirements" description: "The bulk-sender rules Gmail and Yahoo enforce since 2024 — authentication, one-click unsubscribe, and a low complaint rate — and how each is satisfied on Bird." source: https://bird.com/en-us/docs/knowledge-base/deliverability/gmail-yahoo-requirements --- # Gmail & Yahoo sender requirements Since early 2024, Gmail and Yahoo enforce a common set of rules for anyone sending meaningful volume to their users (Gmail draws its bulk-sender line at roughly 5,000 messages a day to Gmail addresses, but the rules are best practice at any volume). Mail that doesn't comply is filtered to spam or rejected outright — these are requirements, not suggestions. The good news: on Bird, each one maps to a step you've likely already taken. ## Requirement 1: Authenticate your mail Bulk senders must authenticate with SPF and DKIM, and publish a DMARC record — and the authentication must **align**, meaning it passes in the name of the same domain your recipients see in the From address. **On Bird:** verifying your sending domain covers all of it. Publishing the DNS records from your domain's detail page gives you DKIM signing in your domain's name and a DMARC record, and the return-path CNAME provides SPF alignment — SPF is evaluated against your bounce domain, which the CNAME points at Bird's infrastructure, so it passes and aligns without you adding an SPF record at your domain's apex. Don't be surprised that Bird never asks you for an apex SPF entry; that's by design. The walkthrough is in the [sending domains guide](/docs/guides/email/sending-domains). Once you've sent a test message, confirm it delivered in the [email log](/docs/guides/email/email-log); if you want to see the SPF, DKIM, and DMARC verdicts the receiving mailbox recorded, paste the received copy's raw source into the [Email header analyzer](/tools/email-analyzer). If you haven't published a DMARC record yet, the [DMARC policy generator](/tools/dmarc-policy) builds a valid one for you. ## Requirement 2: One-click unsubscribe Marketing and promotional mail must carry a one-click unsubscribe option in the message headers (the technical standard is RFC 8058) — the "Unsubscribe" link mail clients show next to the sender name — and opt-outs must be honored within two days. **On Bird:** Bird adds the required one-click List-Unsubscribe headers automatically on marketing mail, which is now the default category, so any broadcast or `marketing` API send carries them, and recipients who use them are suppressed from future marketing mail immediately. Bird also adds a visible unsubscribe link to the HTML of marketing sends, because the header button alone doesn't satisfy the visible opt-out recipients look for, and the ones who can't find a way out click "mark as spam" instead. ## Requirement 3: Keep complaints under 0.3% Senders must keep their spam-complaint rate below 0.3%, and ideally below 0.1%. This is the requirement that demands ongoing attention rather than one-time setup: 0.3% is roughly one complaint per 300 delivered messages, and providers filter aggressively once you cross it. **On Bird:** the Metrics page in the dashboard tracks your complaint rate over time, and Bird automatically suppresses anyone who complains so they're never mailed again. Staying under the line is about list quality — sending to people who opted in and pruning the unengaged. See [sender reputation monitoring](/docs/knowledge-base/deliverability/reputation-monitoring) for the thresholds and what to do when the rate climbs, and the [email consent article](/docs/knowledge-base/compliance/email/consent) for getting permission right in the first place. ![The Metrics page in the Bird dashboard: delivery, open, bounce, and complaint rate cards with health labels and limits, above the delivery and engagement chart](/images/docs/dashboard-email-metrics.png) ## The checklist | Requirement | Bird step | | -------------------------------- | ------------------------------------------------------------------------------------- | | SPF, DKIM, and aligned DMARC | Verify your sending domain — the DNS records cover authentication end to end | | One-click unsubscribe (RFC 8058) | Added automatically on marketing sends (the default), with a visible in-body link too | | Complaint rate under 0.3% | Watch the Metrics page; send to opted-in recipients and prune the unengaged | Beyond these three headline rules, the providers' guidelines also expect basics Bird handles as a matter of course — valid forward and reverse DNS on sending IPs, standards-compliant message formatting — so domain verification plus a clean list is genuinely the whole job. ## Next steps - [Sending domains](/docs/guides/email/sending-domains) — the developer guide to domain verification and the DNS records - [Email consent](/docs/knowledge-base/compliance/email/consent) — getting and keeping permission to mail - [Sender reputation monitoring](/docs/knowledge-base/deliverability/reputation-monitoring) — tracking your complaint rate and reacting to spikes --- title: "List hygiene" description: "Why pruning invalid and inactive addresses protects your deliverability, what Bird suppresses automatically, and the habits that keep a list healthy." source: https://bird.com/en-us/docs/knowledge-base/deliverability/list-hygiene --- # List hygiene Your recipient list is the single biggest input to your deliverability. Mailbox providers watch how recipients react to your mail, and two signals hurt the most: **hard bounces** (the address doesn't exist — a sign of bad or stale data) and **spam complaints** (the recipient marked your mail as junk — a sign it wasn't wanted). Senders with high bounce or complaint rates look like they bought a list or never asked permission, and providers respond by filtering more of their mail to spam. Sustained bad rates can also trigger Bird's own protections, throttling or pausing your sending until the problem is addressed. Keeping the list clean is how you stay out of that territory. ## What Bird handles for you Bird automatically suppresses addresses that hard-bounce or file a spam complaint: they're added to your workspace's suppression list, and future sends to them are blocked before they leave the platform. You never keep mailing a dead address or someone who reported you, even if the address is still sitting in your own database. Unsubscribes are suppressed the same way for marketing mail. Suppressed recipients are visibly rejected rather than silently dropped, so you can always see which addresses were blocked and why. You can browse and manage the list in the dashboard; the technical details are in the [suppressions guide](/docs/guides/email/suppressions). ![The Suppressions page in the Bird dashboard, listing suppressed addresses with their reason and origin](/images/docs/dashboard-email-suppressions.png) ## What you should do yourself Automatic suppression catches the addresses that have already gone bad. The part Bird can't do for you is removing recipients who are quietly losing interest: - **Send only to people who opted in.** Purchased, scraped, or very old lists are where bounces and complaints come from. If someone didn't ask for your mail, don't send it. - **Honor unsubscribes immediately.** Bird blocks future marketing sends to anyone who unsubscribes, but make sure your own systems mirror that — re-importing an old list shouldn't resurrect addresses that opted out. - **Prune chronically unengaged recipients.** Someone who hasn't opened or clicked anything in many months is a deliverability liability: at best they ignore you, and at worst their address has been turned into a spam trap. Either run a re-engagement campaign and remove non-responders, or simply stop mailing them. A smaller, engaged list outperforms a big, quiet one. - **Review your suppression list periodically.** It tells you where your data is going stale. A steady stream of hard bounces from one signup source, for example, points to a form that needs validation or confirmation. ## A simple routine 1. Before any large send, ask where each segment of the list came from and when those people last engaged. 2. After the send, check your bounce and complaint rates on the Metrics page — see [sender reputation monitoring](/docs/knowledge-base/deliverability/reputation-monitoring) for what good looks like. 3. Every few months, define your own inactivity cutoff (for example, no opens or clicks in six months), try once to re-engage those recipients, and remove the ones who stay silent. ## Next steps - [Sender reputation monitoring](/docs/knowledge-base/deliverability/reputation-monitoring) — the bounce and complaint thresholds to watch - [Suppressions](/docs/guides/email/suppressions) — the full technical contract for the suppression list --- title: "Sender reputation monitoring" description: "The health metrics that govern your ability to send: bounce, complaint, and unsubscribe rates, where to watch them, and how to react when they climb." source: https://bird.com/en-us/docs/knowledge-base/deliverability/reputation-monitoring --- # Sender reputation monitoring Your sender reputation is the track record mailbox providers keep on your domain and IPs, and it decides whether your mail reaches the inbox at all. You can't read providers' internal scores directly, but the metrics that drive them are visible in your own sending data — and watching them is how you catch a problem while it's still small. ## The metrics that matter - **Bounce rate** — the share of mail that couldn't be delivered. Hard bounces (the address doesn't exist) are the damaging kind: they signal stale or bad data, and providers read a high hard-bounce rate as the mark of a purchased or scraped list. - **Complaint (spam) rate** — the share of delivered mail that recipients marked as junk. This is the most damaging signal there is: each complaint is a person telling their provider your mail was unwanted. - **Unsubscribe rate** — people opting out. Unsubscribes don't hurt your reputation by themselves — they're the healthy alternative to complaints — but a sudden spike tells you something about a send missed the mark, and the people who didn't bother to unsubscribe may complain instead next time. ## Where to see them The [**Metrics**](https://bird.com/dashboard/w/email/metrics) page in the Bird dashboard shows your delivery, open, bounce, and complaint rates over time, with breakdowns by sending domain and more. ![The Metrics page in the Bird dashboard: delivery, open, bounce, and complaint rates with breakdowns](/images/docs/dashboard-email-metrics.png) Make checking it a habit around your sending cadence — after every large campaign, and at least weekly for a steady transactional stream. The same numbers are available programmatically; see the [tracking and metrics guide](/docs/guides/email/tracking-and-metrics) for the developer view. ## What good looks like - **Complaint rate: well under 0.3%, ideally below 0.1%.** Large providers treat 0.3% as the line where bulk senders get filtered aggressively — that's roughly one complaint per 300 delivered messages, so there is very little margin. A healthy program lives closer to one in a thousand. - **Bounce rate: low single digits.** A couple of percent is normal; sustained rates above that — especially hard bounces — mean your list needs attention. - **Watch the trend, not just the level.** A complaint rate that doubles week over week is a warning even while the absolute number still looks fine. ## What happens when rates stay high Sustained high bounce or complaint rates don't just hurt your standing with mailbox providers — they can trigger Bird's own protections. Sending that consistently generates bad signals may be automatically throttled (slowed down) or paused entirely until the underlying problem is fixed. This protects both your domain's long-term reputation and the shared infrastructure your mail rides on. If that has happened to you, the [throttled or paused article](/docs/knowledge-base/troubleshooting/throttled-paused) explains what it means and how to recover. ## How to react when a metric climbs 1. **Find the source.** Use the Metrics page breakdowns to isolate the problem: one campaign, one sending domain, one audience segment? A spike usually has a single cause. 2. **Stop or shrink the offending send.** Don't keep mailing the segment that's bouncing or complaining while you investigate. 3. **Fix the list.** Bird has already suppressed the addresses that bounced or complained; your job is the rest of the segment they came from — see [list hygiene](/docs/knowledge-base/deliverability/list-hygiene) for the routine. 4. **Rebuild gradually.** Reputation recovers the way it's built: steady volume to engaged recipients. If the damage was significant, treat the recovery like a warmup — start with your best audience and widen slowly, as described in [warming best practices](/docs/knowledge-base/deliverability/warming). ## Next steps - [Why is my sending throttled or paused?](/docs/knowledge-base/troubleshooting/throttled-paused) — what Bird's automatic protections mean and how to recover - [Warming best practices](/docs/knowledge-base/deliverability/warming) — ramping volume safely - [List hygiene](/docs/knowledge-base/deliverability/list-hygiene) — keeping bounces and complaints down at the source - [Tracking & metrics](/docs/guides/email/tracking-and-metrics) — the developer guide to the stats behind the Metrics page --- title: "Avoiding the spam folder" description: "The three levers that decide whether mail lands in the inbox or spam: authentication, content, and list quality, plus the Bird action behind each one." source: https://bird.com/en-us/docs/knowledge-base/deliverability/spam-folder --- # Avoiding the spam folder There is no single trick that keeps mail out of the spam folder. Mailbox providers weigh many signals, but they cluster into three levers you control: **who you are** (authentication), **what you send** (content), and **who you send to** (list quality). Get all three right and inbox placement largely takes care of itself; neglect any one and the other two can't fully compensate. ## Lever 1: Prove who you are Providers junk mail they can't attribute to a real sender. Authentication is how your domain vouches for your messages: when your sending domain is verified with Bird, your mail is signed in your domain's name (DKIM), bounces flow back through an address aligned with your domain (which makes SPF pass and align), and your DMARC policy tells receivers the checks are in force. Recipients see your mail come from your domain, and providers can verify it really did. **The Bird action:** verify your sending domain by publishing the DNS records from the domain's detail page. That single setup step covers DKIM, SPF alignment, and DMARC — the walkthrough is in the [sending domains guide](/docs/guides/email/sending-domains). It's also the core of the [Gmail and Yahoo bulk-sender requirements](/docs/knowledge-base/deliverability/gmail-yahoo-requirements), so it's not optional for any serious sender. If you need a DMARC record to publish, the [DMARC policy generator](/tools/dmarc-policy) builds one. Confirm a test send in the [email log](/docs/guides/email/email-log); the [Email header analyzer](/tools/email-analyzer) can additionally read the received copy's authentication verdicts. ## Lever 2: Send mail that looks legitimate Content filters have moved well beyond keyword lists, but the basics still matter: - **Write honest subject lines.** ALL CAPS, stacked exclamation marks, and "FREE!!!" framing are classic spam markers — and even when they get through, they train recipients to complain. - **Balance text and images.** A message that is one giant image with no real text is a long-standing spam pattern. Include genuine text content, and make sure the message still makes sense with images turned off. - **Include a real, working unsubscribe link** in every marketing message. Counterintuitively, an easy way out keeps you out of the spam folder: recipients who can't find the unsubscribe link click "mark as spam" instead, and complaints damage your reputation far more than unsubscribes do. - **Match what people signed up for.** A sudden pivot in topic, frequency, or tone drives complaints even from genuinely opted-in recipients. ## Lever 3: Send to people who want it Providers watch how recipients react. Mail that gets opened and clicked builds your reputation; mail that bounces or gets reported tears it down. This lever is list quality: - Send only to people who opted in, and remove the chronically unengaged — the full routine is in [list hygiene](/docs/knowledge-base/deliverability/list-hygiene). - Keep bounce and complaint rates low and watch them on the Metrics page; the thresholds and what to do when they climb are in [sender reputation monitoring](/docs/knowledge-base/deliverability/reputation-monitoring). - If your domain or volume is new, ramp up gradually rather than blasting at full volume from day one — see [warming best practices](/docs/knowledge-base/deliverability/warming). ## When mail still lands in spam If authenticated, well-formed mail to an opted-in list still hits the spam folder, the cause is almost always reputation history: a past spike in complaints or bounces, a cold domain, or a sudden volume jump. Check your rates on the Metrics page, slow down, concentrate on your most engaged recipients, and let the good signals accumulate — reputation recovers the same way it was built. ![The Metrics page in the Bird dashboard: delivery, open, bounce, and complaint rate cards with health labels and limits, above the delivery and engagement chart](/images/docs/dashboard-email-metrics.png) ## Next steps - [Gmail & Yahoo sender requirements](/docs/knowledge-base/deliverability/gmail-yahoo-requirements) — the formal rules large providers enforce for bulk senders - [Warming best practices](/docs/knowledge-base/deliverability/warming) — ramping new domains and IPs safely - [List hygiene](/docs/knowledge-base/deliverability/list-hygiene) — keeping bounces and complaints down at the source - [Sending domains](/docs/guides/email/sending-domains) — the developer guide to domain verification --- title: "Warming best practices" description: "Why new sending domains and dedicated IPs need a gradual volume ramp, how Bird warms dedicated IPs automatically, and how to ramp your own sending safely." source: https://bird.com/en-us/docs/knowledge-base/deliverability/warming --- # Warming best practices Mailbox providers like Gmail and Outlook judge your mail by the track record of the domain and IP address it comes from. A brand-new domain or IP has no track record, and a sudden burst of high volume from an unknown sender looks exactly like spam — so providers throttle it or send it to the junk folder. Warming is the fix: start small, increase gradually, and let providers learn that your mail is wanted. It takes a few weeks, and skipping it is the most common way new senders damage their deliverability before they've really started. ## Dedicated IPs warm automatically If you've purchased a [dedicated IP](/docs/guides/email/dedicated-ips-and-pools), the IP warmup itself is handled for you. Every new dedicated IP starts in a warming state, and Bird ramps its volume automatically over roughly 30 days: the share of traffic routed through the new IP grows in stages, and anything beyond what the IP can safely carry overflows through Bird's warmed shared pool. Your mail keeps flowing at full volume the whole time — only the split between the new IP and the shared infrastructure changes. You can watch warmup progress as a percentage from 0 to 100 in the dashboard — each warming IP shows a progress bar that advances steadily through the ramp. There's nothing to configure or schedule; the details of how the automatic ramp works are in the [IP warmup guide](/docs/guides/email/ip-warmup). ![A warming dedicated IP in the Bird dashboard with its warmup progress bar](/images/docs/dashboard-email-dedicated-ips.png) One thing stays in your hands: **don't switch all your traffic onto a still-warming IP.** Buying a dedicated IP never changes where your mail goes by default — the shared pool keeps carrying it until you deliberately flip the default. Leave it that way until the IP's warmup reaches 100%, then switch. Pointing your full volume at a cold IP defeats the purpose of the ramp. ## Domains warm too Warming isn't only about IPs. Mailbox providers track your **domain's** reputation separately, so a brand-new sending domain should also ramp up gradually — even when it sends through Bird's already-warmed shared IPs. If you've just verified a new domain and plan to send serious volume from it, treat the first few weeks as a warmup period rather than going straight to full blast. The same applies when you move an established program to a new domain or subdomain: the old domain's reputation doesn't transfer. Plan the migration as a ramp, shifting traffic over in stages rather than cutting over in one day. ## How to ramp safely - **Start with your most engaged recipients.** Early opens, clicks, and replies are exactly the signals that teach providers your mail is wanted. Begin with people who recently opted in or regularly engage, and widen the audience as the ramp progresses. Save the long tail of older, quieter addresses for after the warmup. - **Increase volume steadily, not in spikes.** A smooth upward curve — roughly doubling every few days is a common pattern — reads as organic growth. A flat week followed by a 10x day reads as a hijacked sender. - **Watch your bounce and complaint rates as you go.** Rising bounces or complaints during a warmup mean you're widening the audience too fast or mailing people who didn't ask for it. Check the [reputation metrics](/docs/knowledge-base/deliverability/reputation-monitoring) regularly during the ramp, and slow down if the rates climb. - **Keep sending consistently once you're warmed.** Reputation decays with silence. A domain or IP that goes quiet for weeks partially loses the trust it earned, so a steady cadence beats occasional bursts. ## Next steps - [IP warmup](/docs/guides/email/ip-warmup) — how the automatic 30-day ramp works in detail - [Dedicated IPs and pools](/docs/guides/email/dedicated-ips-and-pools) — purchasing IPs and default-pool rules - [Sender reputation monitoring](/docs/knowledge-base/deliverability/reputation-monitoring) — the metrics to watch while you ramp --- title: "Add DNS records · Azure DNS" description: "Click-by-click instructions for publishing Bird's sending-domain DNS records in an Azure DNS zone via the Azure portal." source: https://bird.com/en-us/docs/knowledge-base/dns-and-domains/azure --- # Add DNS records · Azure DNS <!-- external-screenshot: Azure portal → DNS zones → your zone → Record sets --> If your domain's DNS is hosted in an Azure DNS zone, this page walks you through publishing the records Bird asks for on a sending domain: a DKIM TXT record, a return-path CNAME, an optional tracking CNAME, and a DMARC TXT record. Bird does not ask you to add an SPF record at your domain's apex — see [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc) for why. Before you start, open your domain's detail page in the Bird dashboard and keep it visible — every host and value below is copied from there, not typed by hand. ![A domain's DNS Records page in the Bird dashboard, showing the verified DKIM record with copyable name and value, and the return-path and DMARC sections below](/images/docs/dashboard-email-domain-detail.png) ## Find your DNS zone 1. Sign in to the [Azure portal](https://portal.azure.com). 2. In the search bar at the top, type **DNS zones** and select it from the results. 3. Click the zone that matches your sending domain (for example `example.com`). 4. You land on the zone's overview, which lists the existing record sets. Each record below is added with the **+ Record set** button at the top. ## Add the records For each record, click **+ Record set** and fill in the pane that opens on the right. The **Name** field is relative to the zone — enter only the part before your domain name (and `@` would mean the zone apex itself, which none of Bird's records use). ### DKIM (TXT) 1. Click **+ Record set**. 2. **Name**: paste the DKIM host from Bird, but remove your domain from the end — for `example.com` you enter `<selector>._domainkey`, not the full hostname. 3. **Type**: select **TXT** from the dropdown. 4. **Value**: paste the DKIM value exactly as shown in Bird (`v=DKIM1; k=rsa; p=...`). If Azure rejects it as too long for a single string, run it through the [DNS record splitter](/tools/dns-record-splitter) to get quoted chunks it will accept. 5. Leave TTL at the default and click **OK** (or **Add** in the newer portal experience). ### Return-path (CNAME) 1. Click **+ Record set**. 2. **Name**: `send` (or whatever subdomain your Bird dashboard shows before your domain name). 3. **Type**: select **CNAME**. 4. **Alias**: paste the value from Bird, for example `<region>.bounce.bird.com`. 5. Click **OK**. ### Tracking (CNAME, optional) This record only enables branded open and click tracking — your domain can verify and send without it. 1. Click **+ Record set**. 2. **Name**: `links` (or the subdomain shown in Bird). 3. **Type**: select **CNAME**. 4. **Alias**: paste the value from Bird, for example `<region>.links.bird.com`. 5. Click **OK**. ### DMARC (TXT) 1. Click **+ Record set**. 2. **Name**: `_dmarc`. 3. **Type**: select **TXT**. 4. **Value**: paste the DMARC value from Bird, for example `v=DMARC1; p=none; rua=mailto:example.com@dmarc.bird.com;`. 5. Click **OK**. If you already have a DMARC record at this domain or a parent domain, you can keep it — Bird only requires that a valid DMARC record exists. ## Azure gotchas - **Names are relative to the zone.** Azure appends the zone name to whatever you type in the Name field — entering `send.example.com` in the `example.com` zone creates `send.example.com.example.com`. Enter only the leading label(s); `@` is reserved for the zone apex. - **Pick the type before you fill in values.** The pane's value fields change with the record type dropdown — if you do not see an Alias field for a CNAME or a Value box for a TXT record, check the type selection first. - **A CNAME record set holds exactly one value.** TXT and most other Azure record sets accept multiple values, but a CNAME record set cannot — and Azure will also refuse a CNAME at a name where other records already exist. If the portal rejects the record, check for a conflicting record set at the same name. - **The zone only matters if it is delegated.** An Azure DNS zone answers queries only when your registrar's name servers point at it. On the zone's overview, copy the four `*.azure-dns.*` name servers and confirm they match the NS records set at your registrar — otherwise the records you add here are invisible to the rest of the internet. ## After you publish There is nothing to click in Bird — verification is automatic. Bird starts checking your DNS within seconds of the domain being added and re-checks frequently, so most domains flip to verified within minutes of the records going live. You can also trigger an immediate re-check from the domain's detail page in the dashboard. If the domain stays pending longer than you expect, see [Verification delays](/docs/knowledge-base/dns-and-domains/verification-delays). ## Next steps - [Sending domains](/docs/guides/email/sending-domains) — adding and managing domains end to end - [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc) — what each record proves and why there is no apex SPF record - [Verification delays](/docs/knowledge-base/dns-and-domains/verification-delays) — what to check when a domain stays pending --- title: "Add DNS records · Cloudflare" description: "Publish Bird's sending-domain DNS records in the Cloudflare dashboard, step by step." source: https://bird.com/en-us/docs/knowledge-base/dns-and-domains/cloudflare --- # Add DNS records · Cloudflare <!-- external-screenshot: Cloudflare → DNS → Records → Add record --> When you add a sending domain to Bird, the domain's detail page in the Bird dashboard lists every DNS record to publish, each with a copyable host and value. Keep that page open in one tab and the Cloudflare dashboard in another — every value below is copied straight from Bird, never typed by hand. ![A domain's DNS Records page in the Bird dashboard, showing the verified DKIM record with copyable name and value, and the return-path and DMARC sections below](/images/docs/dashboard-email-domain-detail.png) ## The records you'll add | Type | Host | Value (copy from Bird) | | ----- | ----------------------------------- | -------------------------- | | TXT | `<selector>._domainkey.example.com` | Your DKIM key | | CNAME | `send.example.com` | `<region>.bounce.bird.com` | | TXT | `_dmarc.example.com` | Your DMARC policy | | CNAME | `links.example.com` (optional) | `<region>.links.bird.com` | The tracking CNAME (`links`) is optional — it only enables branded open and click tracking, not sending itself. ## Add the records in Cloudflare 1. Log in to the [Cloudflare dashboard](https://dash.cloudflare.com) and select the account that holds your domain. 2. Click your domain in the list, then choose **DNS** → **Records** in the left sidebar. 3. Click **Add record**. 4. For the DKIM record: set **Type** to **TXT**, paste the host from Bird into **Name**, and paste the DKIM value into **Content**. If the value is too long for Cloudflare to accept as one string, run it through the [DNS record splitter](/tools/dns-record-splitter) first. Leave **TTL** on **Auto**. Click **Save**. 5. Click **Add record** again. For the return-path record: set **Type** to **CNAME**, enter the host (for example `send`), paste the target from Bird (for example `<region>.bounce.bird.com`) into **Target**, and switch **Proxy status** to **DNS only** (the cloud icon turns grey). Click **Save**. 6. Click **Add record** again. For the DMARC record: set **Type** to **TXT**, enter `_dmarc` as the **Name**, and paste the DMARC value from Bird into **Content**. Click **Save**. 7. If you want branded tracking, add the optional tracking record the same way as step 5: **Type** **CNAME**, **Name** `links`, **Target** `<region>.links.bird.com`, **Proxy status** **DNS only**. Click **Save**. ## Cloudflare-specific gotchas - **Turn the orange cloud off.** Cloudflare proxies CNAME records by default (orange cloud, "Proxied"). A proxied record answers with Cloudflare's IPs instead of Bird's hostnames, which breaks bounce routing and stops verification from ever succeeding. Both CNAME records must be set to **DNS only** (grey cloud). - **Watch the Name field.** Cloudflare accepts both a bare label (`send`) and a fully qualified name (`send.example.com`) and appends the zone when needed — but if you paste a full name into a field where you already typed part of it, you can end up with `send.example.com.example.com`. After saving, check that the record's name reads exactly as Bird shows it. - **Propagation is fast.** Cloudflare publishes changes to its nameservers within seconds, so there is rarely any waiting once the records are saved correctly. ## After you publish Bird checks your DNS automatically — verification usually completes within minutes of the records going live, and you can trigger an immediate re-check from the domain's detail page. If the domain stays pending longer than expected, see [Verification delays](/docs/knowledge-base/dns-and-domains/verification-delays). ## Next steps - [Sending domains](/docs/guides/email/sending-domains) - [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc) - [Verification delays](/docs/knowledge-base/dns-and-domains/verification-delays) --- title: "Add DNS records · DigitalOcean" description: "Click-by-click instructions for publishing Bird's sending-domain DNS records in DigitalOcean's Networking → Domains panel." source: https://bird.com/en-us/docs/knowledge-base/dns-and-domains/digitalocean --- # Add DNS records · DigitalOcean <!-- external-screenshot: DigitalOcean control panel → Networking → Domains → Create new record --> If your domain's DNS is hosted at DigitalOcean, this page walks you through publishing the records Bird asks for on a sending domain: a DKIM TXT record, a return-path CNAME, an optional tracking CNAME, and a DMARC TXT record. Bird does not ask you to add an SPF record at your domain's apex — see [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc) for why. Before you start, open your domain's detail page in the Bird dashboard and keep it visible — every host and value below is copied from there, not typed by hand. ![A domain's DNS Records page in the Bird dashboard, showing the verified DKIM record with copyable name and value, and the return-path and DMARC sections below](/images/docs/dashboard-email-domain-detail.png) ## Find the DNS editor 1. Sign in to the [DigitalOcean control panel](https://cloud.digitalocean.com). 2. In the left sidebar, click **Networking**, then the **Domains** tab. 3. Click your domain in the list. You land on its records page, with a **Create new record** form at the top. The form has a row of tabs for the record type — **A**, **AAAA**, **CNAME**, **TXT**, and so on. Each record below starts by picking the right tab. ## Add the records The **Hostname** field is relative to your domain — enter only the part before your domain name. DigitalOcean appends the domain for you. ### DKIM (TXT) 1. Select the **TXT** tab in the Create new record form. 2. **Hostname**: paste the DKIM host from Bird, but remove your domain from the end — for `example.com` you enter `<selector>._domainkey`. 3. **Value**: paste the DKIM value exactly as shown in Bird (`v=DKIM1; k=rsa; p=...`). If DigitalOcean's field won't accept it as one string, use the [DNS record splitter](/tools/dns-record-splitter) to break it into quoted chunks first. 4. Click **Create Record**. ### Return-path (CNAME) 1. Select the **CNAME** tab. 2. **Hostname**: `send` (or whatever subdomain your Bird dashboard shows before your domain name). 3. **Is an alias of**: paste the value from Bird, for example `<region>.bounce.bird.com`. 4. Click **Create Record**. ### Tracking (CNAME, optional) This record only enables branded open and click tracking — your domain can verify and send without it. 1. Select the **CNAME** tab. 2. **Hostname**: `links` (or the subdomain shown in Bird). 3. **Is an alias of**: paste the value from Bird, for example `<region>.links.bird.com`. 4. Click **Create Record**. ### DMARC (TXT) 1. Select the **TXT** tab. 2. **Hostname**: `_dmarc`. 3. **Value**: paste the DMARC value from Bird, for example `v=DMARC1; p=none; rua=mailto:example.com@dmarc.bird.com;`. 4. Click **Create Record**. If you already have a DMARC record at this domain or a parent domain, you can keep it — Bird only requires that a valid DMARC record exists. ## DigitalOcean gotchas - **Hostnames are relative.** DigitalOcean appends your domain to the Hostname field automatically — entering `send.example.com` creates `send.example.com.example.com`. Type only `send`. - **The trailing dot is normal.** After you save a CNAME, the records list shows the target with a trailing dot (`<region>.bounce.bird.com.`). That is standard fully-qualified DNS notation, not an error — do not try to remove it. - **Records here only work on DigitalOcean's name servers.** The domain must be delegated to `ns1.digitalocean.com`, `ns2.digitalocean.com`, and `ns3.digitalocean.com` at your registrar. If the name servers point elsewhere, records added in this panel have no effect — add them at the provider the name servers actually point to. ## After you publish There is nothing to click in Bird — verification is automatic. Bird starts checking your DNS within seconds of the domain being added and re-checks frequently, so most domains flip to verified within minutes of the records going live. You can also trigger an immediate re-check from the domain's detail page in the dashboard. If the domain stays pending longer than you expect, see [Verification delays](/docs/knowledge-base/dns-and-domains/verification-delays). ## Next steps - [Sending domains](/docs/guides/email/sending-domains) — adding and managing domains end to end - [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc) — what each record proves and why there is no apex SPF record - [Verification delays](/docs/knowledge-base/dns-and-domains/verification-delays) — what to check when a domain stays pending --- title: "Add DNS records · any registrar" description: "A provider-agnostic walkthrough for publishing Bird's sending-domain DNS records at any registrar: finding the zone editor and copying host and value fields." source: https://bird.com/en-us/docs/knowledge-base/dns-and-domains/generic-registrar --- # Add DNS records · any registrar <!-- external-screenshot: a typical registrar DNS management page with an Add record form --> Every DNS provider's control panel looks different, but they all do the same job: store records by name, type, and value. This page is for any registrar not covered by our provider-specific guides — it explains where the DNS editor usually hides, what each Bird record is, and the conventions that trip people up. The records Bird asks for on a sending domain are a DKIM TXT record, a return-path CNAME, an optional tracking CNAME, and a DMARC TXT record. Bird does not ask you to add an SPF record at your domain's apex — see [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc) for why. Before you start, open your domain's detail page in the Bird dashboard and keep it visible — every host and value is copied from there, not typed by hand. ![A domain's DNS Records page in the Bird dashboard, showing the verified DKIM record with copyable name and value, and the return-path and DMARC sections below](/images/docs/dashboard-email-domain-detail.png) ## Step 1: find the DNS editor Sign in to your registrar and open your domain. The DNS editor is usually behind a link named **DNS Management**, **Advanced DNS**, **DNS settings**, **Zone File**, or **Manage DNS** — it is the page that lists records in rows of type, host/name, and value, with an Add button. ## Step 2: confirm the name servers point here DNS records only take effect at the provider your domain's **name servers** point to. Most registrars show the name servers on the domain's overview page. If they are the registrar's own defaults, the DNS editor you just found is the right place. If they point to another provider — Cloudflare, your web host, a cloud DNS service — add the records there instead; anything you add at the registrar will be ignored. ## Step 3: add the records You will add four records (the tracking one is optional). For each, find the Add record (or Add new record) button, pick the type, and fill in the host and value from your Bird domain detail page. ### Which records are which type | Record | Type | Host | Value (copy from Bird) | | ----------- | ----- | ----------------------------------- | ---------------------------------------------------------- | | DKIM | TXT | `<selector>._domainkey.example.com` | `v=DKIM1; k=rsa; p=<public-key>` | | Return-path | CNAME | `send.example.com` | `<region>.bounce.bird.com` | | Tracking\* | CNAME | `links.example.com` | `<region>.links.bird.com` | | DMARC | TXT | `_dmarc.example.com` | `v=DMARC1; p=none; rua=mailto:example.com@dmarc.bird.com;` | \*Optional — it only enables branded open and click tracking; your domain can verify and send without it. The two CNAME records point a hostname at Bird's infrastructure; the two TXT records publish text that receiving mail servers read. Pick the type from your provider's dropdown or tabs before filling in the rest — the form fields usually change with the type. ### How the host/name field works This is where providers differ most, and where most mistakes happen: - **Most providers want the host relative to your domain.** If the form already shows your domain next to the field (or appends it on save), enter only the leading part: `send`, `links`, `_dmarc`, `<selector>._domainkey`. - **Some want the fully qualified name.** If saving `send` produces a record at literally `send` with no domain, or the provider is a raw zone-file editor, use the full hostname (`send.example.com`) — possibly with a trailing dot, which in zone files means "this name is complete, do not append the domain". - **`@` means the domain itself (the root or apex).** You will see it as a placeholder or in existing records. None of Bird's records go at the apex, so you should never need to enter `@` for this setup. - **When in doubt, save one record and look at the result.** If the records list shows `send.example.com.example.com`, you entered a full name where a relative one was expected — edit it down to `send`. ### Copy values exactly Paste each value from the Bird dashboard without retyping or editing: - The DKIM value is a long machine-generated key — a single missing character invalidates it. If your provider has a length limit and Bird shows the value split into multiple quoted strings, paste it as shown; that is standard DNS formatting. If your provider doesn't split it for you, the [DNS record splitter](/tools/dns-record-splitter) will chunk a long TXT value into quoted strings that fit. - Whether TXT values need surrounding quotes varies by provider — most add them for you, so start without quotes and only add them if the provider's docs say to. - CNAME values are hostnames, not URLs — no `https://`, no trailing slash. A trailing dot shown after saving is normal. - Leave TTL at the provider's default; it only affects how fast future changes propagate. ## Step 4: after you publish There is nothing to click in Bird — verification is automatic. Bird starts checking your DNS within seconds of the domain being added and re-checks frequently, so most domains flip to verified within minutes of the records going live. You can also trigger an immediate re-check from the domain's detail page in the dashboard. If the domain stays pending longer than you expect, see [Verification delays](/docs/knowledge-base/dns-and-domains/verification-delays); once mail is flowing, send yourself a test message and check it in the [email log](/docs/guides/email/email-log) (the [Email header analyzer](/tools/email-analyzer) can additionally read the received copy's SPF, DKIM, and DMARC verdicts). ## Provider-specific guides If your provider is one of these, the dedicated walkthrough matches its exact screens: - [Cloudflare](/docs/knowledge-base/dns-and-domains/cloudflare) - [Amazon Route 53](/docs/knowledge-base/dns-and-domains/route-53) - [GoDaddy](/docs/knowledge-base/dns-and-domains/godaddy) - [Namecheap](/docs/knowledge-base/dns-and-domains/namecheap) - [Google Domains / Squarespace](/docs/knowledge-base/dns-and-domains/google-domains) - [Azure DNS](/docs/knowledge-base/dns-and-domains/azure) - [DigitalOcean](/docs/knowledge-base/dns-and-domains/digitalocean) ## Next steps - [Sending domains](/docs/guides/email/sending-domains) — adding and managing domains end to end - [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc) — what each record proves and why there is no apex SPF record - [Verification delays](/docs/knowledge-base/dns-and-domains/verification-delays) — what to check when a domain stays pending --- title: "Add DNS records · GoDaddy" description: "Publish Bird's sending-domain DNS records in GoDaddy's DNS manager, step by step." source: https://bird.com/en-us/docs/knowledge-base/dns-and-domains/godaddy --- # Add DNS records · GoDaddy <!-- external-screenshot: GoDaddy → My Products → Domain → DNS → Add New Record --> When you add a sending domain to Bird, the domain's detail page in the Bird dashboard lists every DNS record to publish, each with a copyable host and value. Keep that page open in one tab and GoDaddy in another — every value below is copied straight from Bird, never typed by hand. ![A domain's DNS Records page in the Bird dashboard, showing the verified DKIM record with copyable name and value, and the return-path and DMARC sections below](/images/docs/dashboard-email-domain-detail.png) ## The records you'll add | Type | Host | Value (copy from Bird) | | ----- | ----------------------------------- | -------------------------- | | TXT | `<selector>._domainkey.example.com` | Your DKIM key | | CNAME | `send.example.com` | `<region>.bounce.bird.com` | | TXT | `_dmarc.example.com` | Your DMARC policy | | CNAME | `links.example.com` (optional) | `<region>.links.bird.com` | The tracking CNAME (`links`) is optional — it only enables branded open and click tracking, not sending itself. ## Add the records in GoDaddy 1. Log in to [GoDaddy](https://www.godaddy.com) and open **My Products**. 2. Find your domain in the list and click **DNS** (or click the domain, then choose the **DNS** tab). 3. Click **Add New Record**. 4. For the DKIM record: set **Type** to **TXT**, enter the host's subdomain part (everything before your domain, for example `<selector>._domainkey`) in **Name**, paste the DKIM value from Bird into **Value**, and set **TTL** to **600 seconds** (the lowest option). GoDaddy caps how much text a single TXT value accepts — if it rejects the paste, run the value through the [DNS record splitter](/tools/dns-record-splitter) to get chunks it will take. Click **Save**. 5. Click **Add New Record** again. For the return-path record: set **Type** to **CNAME**, enter `send` in **Name**, paste the target from Bird (for example `<region>.bounce.bird.com`) into **Value**, and set **TTL** to **600 seconds**. Click **Save**. 6. Click **Add New Record** again. For the DMARC record: set **Type** to **TXT**, enter `_dmarc` in **Name**, paste the DMARC value from Bird into **Value**, and set **TTL** to **600 seconds**. Click **Save**. 7. If you want branded tracking, add the optional tracking record the same way as step 5: **Type** **CNAME**, **Name** `links`, **Value** `<region>.links.bird.com`. Click **Save**. ## GoDaddy-specific gotchas - **The Name field is relative to your domain.** Enter `send`, not `send.example.com` — GoDaddy appends the domain for you, and entering the full name publishes `send.example.com.example.com`. Use `@` only when a record belongs at the domain root (none of Bird's records do). - **Pick the lowest TTL.** GoDaddy defaults to a 1-hour TTL; choosing **600 seconds** means any typo you fix is re-checked within minutes instead of an hour, which speeds up verification. - **Remove conflicting "Parked" records.** Domains that have never hosted anything often carry GoDaddy's parking records. If an existing record sits at the same name as one you are adding (especially a CNAME at `send` or `links`), delete the parked record first — DNS does not allow a CNAME to coexist with another record at the same name. ## After you publish Bird checks your DNS automatically — verification usually completes within minutes of the records going live, and you can trigger an immediate re-check from the domain's detail page. If the domain stays pending longer than expected, see [Verification delays](/docs/knowledge-base/dns-and-domains/verification-delays). ## Next steps - [Sending domains](/docs/guides/email/sending-domains) - [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc) - [Verification delays](/docs/knowledge-base/dns-and-domains/verification-delays) --- title: "Add DNS records · Google Domains / Squarespace" description: "Click-by-click instructions for publishing Bird's sending-domain DNS records in Squarespace Domains, where Google Domains now lives." source: https://bird.com/en-us/docs/knowledge-base/dns-and-domains/google-domains --- # Add DNS records · Google Domains / Squarespace <!-- external-screenshot: Squarespace Domains → DNS settings → Custom records --> Google Domains was acquired by Squarespace, and domains registered there have been migrated to Squarespace Domains — the DNS editor you may remember from Google now lives in your Squarespace account. This page walks you through publishing the DNS records Bird asks for on a sending domain: a DKIM TXT record, a return-path CNAME, an optional tracking CNAME, and a DMARC TXT record. Bird does not ask you to add an SPF record at your domain's apex — see [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc) for why. Before you start, open your domain's detail page in the Bird dashboard and keep it visible — every host and value below is copied from there, not typed by hand. ![A domain's DNS Records page in the Bird dashboard, showing the verified DKIM record with copyable name and value, and the return-path and DMARC sections below](/images/docs/dashboard-email-domain-detail.png) ## Find the DNS editor 1. Sign in to your Squarespace account at [domains.squarespace.com](https://domains.squarespace.com) — this works even if you only ever used Google Domains and have never built a Squarespace site. If your domain has not been claimed since the migration, Squarespace will prompt you to claim it with your Google sign-in. 2. Click **Domains** and select your domain from the list. 3. Click **DNS settings** (in some views this appears as just **DNS**), then scroll to **Custom records**. ## Add the records For each record on your Bird domain detail page, click **Add record** under Custom records and fill in the fields. The **Host** field in Squarespace is relative to your domain — enter only the part before your domain name. ### DKIM (TXT) 1. Click **Add record**. 2. **Host**: paste the DKIM host from Bird, but remove your domain from the end — for `example.com` you enter `<selector>._domainkey`, not `<selector>._domainkey.example.com`. 3. **Type**: select **TXT**. 4. **Data**: paste the DKIM value exactly as shown in Bird (`v=DKIM1; k=rsa; p=...`). Do not add quotes around it — Squarespace stores the raw text and quotes it on the wire for you. If the field won't accept the full value in one go, use the [DNS record splitter](/tools/dns-record-splitter) to break it into chunks first. 5. Click **Save** (or press Enter to confirm the row). ### Return-path (CNAME) 1. Click **Add record**. 2. **Host**: `send` (or whatever subdomain your Bird dashboard shows before your domain name). 3. **Type**: select **CNAME**. 4. **Data**: paste the value from Bird, for example `<region>.bounce.bird.com`. 5. Save the record. ### Tracking (CNAME, optional) This record only enables branded open and click tracking — your domain can verify and send without it. 1. Click **Add record**. 2. **Host**: `links` (or the subdomain shown in Bird). 3. **Type**: select **CNAME**. 4. **Data**: paste the value from Bird, for example `<region>.links.bird.com`. 5. Save the record. ### DMARC (TXT) 1. Click **Add record**. 2. **Host**: `_dmarc`. 3. **Type**: select **TXT**. 4. **Data**: paste the DMARC value from Bird, for example `v=DMARC1; p=none; rua=mailto:example.com@dmarc.bird.com;` — again, no surrounding quotes. 5. Save the record. If you already have a DMARC record at this domain or a parent domain, you can keep it — Bird only requires that a valid DMARC record exists. ## Squarespace gotchas - **The editor moved.** If you go looking for DNS at domains.google.com, you will be redirected — DNS for migrated domains is managed only at Squarespace (**account → Domains → your domain → DNS settings → Custom records**). - **Host names are relative.** Squarespace appends your domain to whatever you type in the Host field. Entering the full hostname (`send.example.com`) creates a broken record at `send.example.com.example.com` — enter only `send`. - **Paste TXT values without quotes.** Some providers require quoted TXT values; Squarespace does not. Paste the DKIM and DMARC values exactly as Bird shows them, with no `"` added. - **Third-party name servers override everything.** If your domain's name servers point somewhere else (Cloudflare, for example), records added in Squarespace have no effect — add them at whichever provider the name servers point to instead. ## After you publish There is nothing to click in Bird — verification is automatic. Bird starts checking your DNS within seconds of the domain being added and re-checks frequently, so most domains flip to verified within minutes of the records going live. You can also trigger an immediate re-check from the domain's detail page in the dashboard. If the domain stays pending longer than you expect, see [Verification delays](/docs/knowledge-base/dns-and-domains/verification-delays). ## Next steps - [Sending domains](/docs/guides/email/sending-domains) — adding and managing domains end to end - [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc) — what each record proves and why there is no apex SPF record - [Verification delays](/docs/knowledge-base/dns-and-domains/verification-delays) — what to check when a domain stays pending --- title: "Add DNS records · Namecheap" description: "Publish Bird's sending-domain DNS records in Namecheap's Advanced DNS tab, step by step." source: https://bird.com/en-us/docs/knowledge-base/dns-and-domains/namecheap --- # Add DNS records · Namecheap <!-- external-screenshot: Namecheap → Domain List → Manage → Advanced DNS → Add New Record --> When you add a sending domain to Bird, the domain's detail page in the Bird dashboard lists every DNS record to publish, each with a copyable host and value. Keep that page open in one tab and Namecheap in another — every value below is copied straight from Bird, never typed by hand. ![A domain's DNS Records page in the Bird dashboard, showing the verified DKIM record with copyable name and value, and the return-path and DMARC sections below](/images/docs/dashboard-email-domain-detail.png) ## The records you'll add | Type | Host | Value (copy from Bird) | | ----- | ----------------------------------- | -------------------------- | | TXT | `<selector>._domainkey.example.com` | Your DKIM key | | CNAME | `send.example.com` | `<region>.bounce.bird.com` | | TXT | `_dmarc.example.com` | Your DMARC policy | | CNAME | `links.example.com` (optional) | `<region>.links.bird.com` | The tracking CNAME (`links`) is optional — it only enables branded open and click tracking, not sending itself. ## Add the records in Namecheap 1. Log in to [Namecheap](https://www.namecheap.com) and open **Domain List** in the left sidebar. 2. Click **Manage** next to your domain, then open the **Advanced DNS** tab. 3. Under **Host Records**, click **Add New Record**. 4. For the DKIM record: choose **TXT Record** as the type, enter the host's subdomain part (everything before your domain, for example `<selector>._domainkey`) in **Host**, paste the DKIM value from Bird into **Value**, and leave **TTL** on **Automatic**. If Namecheap won't take the full value in one field, the [DNS record splitter](/tools/dns-record-splitter) breaks it into chunks that fit. Click the green checkmark to save. 5. Click **Add New Record** again. For the return-path record: choose **CNAME Record**, enter `send` in **Host**, paste the target from Bird (for example `<region>.bounce.bird.com`) into **Value**, and save. 6. Click **Add New Record** again. For the DMARC record: choose **TXT Record**, enter `_dmarc` in **Host**, paste the DMARC value from Bird into **Value**, and save. 7. If you want branded tracking, add the optional tracking record the same way as step 5: **CNAME Record**, **Host** `links`, **Value** `<region>.links.bird.com`, and save. ## Namecheap-specific gotchas - **Host is the subdomain only.** Namecheap appends your domain to whatever you enter, so type `send`, not `send.example.com` — the full name publishes `send.example.com.example.com`. Use `@` only for records at the domain root (none of Bird's records need it). - **"Automatic" TTL is 30 minutes.** Namecheap's default TTL means a corrected record can take up to half an hour to be seen; if you want faster turnaround on fixes, pick a shorter TTL from the dropdown before saving. - **Advanced DNS only works on Namecheap BasicDNS.** The Host Records table only takes effect when the domain's **Nameservers** setting (on the **Domain** tab) is **Namecheap BasicDNS**. If the domain points at custom nameservers — Cloudflare, your host, anywhere else — add the records at that provider instead; anything you enter here is ignored. ## After you publish Bird checks your DNS automatically — verification usually completes within minutes of the records going live, and you can trigger an immediate re-check from the domain's detail page. If the domain stays pending longer than expected, see [Verification delays](/docs/knowledge-base/dns-and-domains/verification-delays). ## Next steps - [Sending domains](/docs/guides/email/sending-domains) - [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc) - [Verification delays](/docs/knowledge-base/dns-and-domains/verification-delays) --- title: "Add DNS records · AWS Route 53" description: "Publish Bird's sending-domain DNS records in an AWS Route 53 hosted zone, step by step." source: https://bird.com/en-us/docs/knowledge-base/dns-and-domains/route-53 --- # Add DNS records · AWS Route 53 <!-- external-screenshot: Route 53 → Hosted zones → example.com → Create record --> When you add a sending domain to Bird, the domain's detail page in the Bird dashboard lists every DNS record to publish, each with a copyable host and value. Keep that page open in one tab and the AWS console in another — every value below is copied straight from Bird, never typed by hand. ![A domain's DNS Records page in the Bird dashboard, showing the verified DKIM record with copyable name and value, and the return-path and DMARC sections below](/images/docs/dashboard-email-domain-detail.png) ## The records you'll add | Type | Host | Value (copy from Bird) | | ----- | ----------------------------------- | -------------------------- | | TXT | `<selector>._domainkey.example.com` | Your DKIM key | | CNAME | `send.example.com` | `<region>.bounce.bird.com` | | TXT | `_dmarc.example.com` | Your DMARC policy | | CNAME | `links.example.com` (optional) | `<region>.links.bird.com` | The tracking CNAME (`links`) is optional — it only enables branded open and click tracking, not sending itself. ## Add the records in Route 53 1. Sign in to the [AWS console](https://console.aws.amazon.com/route53/) and open **Route 53**. 2. In the left sidebar, choose **Hosted zones**, then click the **public** hosted zone whose name matches your sending domain. 3. Click **Create record**. 4. For the DKIM record: paste the host's subdomain part from Bird (everything before your domain, for example `<selector>._domainkey`) into **Record name**, set **Record type** to **TXT**, and paste the DKIM value from Bird into **Value**. Keep **Routing policy** on **Simple routing**. Click **Create records**. 5. Click **Create record** again. For the return-path record: enter `send` as the **Record name**, set **Record type** to **CNAME**, and paste the target from Bird (for example `<region>.bounce.bird.com`) into **Value**. Click **Create records**. 6. Click **Create record** again. For the DMARC record: enter `_dmarc` as the **Record name**, set **Record type** to **TXT**, and paste the DMARC value from Bird into **Value**. Click **Create records**. 7. If you want branded tracking, add the optional tracking record the same way as step 5: **Record name** `links`, **Record type** **CNAME**, **Value** `<region>.links.bird.com`. Click **Create records**. ## Route 53-specific gotchas - **Don't add your own quotes to TXT values.** Route 53 wraps TXT values in double quotes automatically. Paste the value exactly as Bird shows it — if you add quotes yourself, the published record contains literal quote characters and verification fails. Route 53 also caps a single quoted string at 255 characters; if the DKIM value is longer, use the [DNS record splitter](/tools/dns-record-splitter) to break it into the multiple quoted strings a TXT record can hold. - **Make sure the zone is actually serving the domain.** It's common to have a hosted zone that exists in Route 53 while the domain's nameservers point somewhere else (an old registrar, a second AWS account, a duplicate zone). Compare the zone's **NS** record set against the nameservers your registrar lists for the domain — if they differ, records you add here are never seen by the internet and the domain never verifies. - **Use Simple routing.** Leave **Routing policy** on **Simple routing** for all four records; weighted, latency, or failover policies are not needed and only add ways for the lookup to go wrong. ## After you publish Bird checks your DNS automatically — verification usually completes within minutes of the records going live, and you can trigger an immediate re-check from the domain's detail page. If the domain stays pending longer than expected, see [Verification delays](/docs/knowledge-base/dns-and-domains/verification-delays). ## Next steps - [Sending domains](/docs/guides/email/sending-domains) - [DKIM, SPF & DMARC](/docs/guides/email/dkim-spf-dmarc) - [Verification delays](/docs/knowledge-base/dns-and-domains/verification-delays) --- title: "Sending from the shared domain" description: "What Bird's shared onboarding domain is for, who it can deliver to, and when to move to your own verified domain." source: https://bird.com/en-us/docs/knowledge-base/dns-and-domains/shared-domain --- # Sending from the shared domain Every new Bird workspace can send email immediately from **Bird's shared onboarding domain** — messages from `onboarding@messagebird.dev`. There's nothing to configure: no DNS records, no domain verification, no waiting. It exists so you can prove the pipes work in your first five minutes, before you've set up anything of your own. Because the domain is shared by everyone, it comes with deliberate guardrails. This page explains what they are and why they're there. ## What it's for The shared domain is a **test and first-send** capability: - Confirming your account and workspace can send, right after signup. - Letting a developer verify an integration end to end before your own domain is verified. - Running deterministic delivery tests against the [mail sandbox](/docs/guides/email/testing-sandbox). It is not meant for production sending — not because of an arbitrary rule, but because the guardrails below make it unsuitable for real traffic by design. ## The guardrails **Recipients must be verified members of your workspace.** The shared domain only delivers to people who have a verified Bird account with access to your workspace — in practice, you and your teammates. Sending to any other address is rejected with a clear error explaining why. This protects the shared domain's reputation for everyone using it: nobody can use it to email strangers. **There's a daily recipient cap.** Sends are capped per organization per day — by default **50 recipients across your whole organization**. Once you hit the cap, further sends are rejected with a clear error, and the allowance resets the next day. Fifty is plenty for testing; it's intentionally far too small for production. **One exception: sandbox addresses.** The [mail sandbox addresses](/docs/guides/email/testing-sandbox) — `delivered@`, `bounce@`, and the other magic addresses at `messagebird.dev` — are exempt from the members-only restriction, so you can simulate deliveries, bounces, and complaints without inviting anyone. They do still count toward the daily cap. ## Moving to production: verify your own domain When you're ready to send real email to real recipients, set up your own sending domain. That removes the recipient restriction and the daily cap, puts your own name in the From address, and builds sending reputation that belongs to you. The [Sending domains guide](/docs/guides/email/sending-domains) walks through registering a domain, publishing its DNS records, and the verification lifecycle — most domains verify within minutes of DNS propagation. The shared domain doesn't go away when you verify your own — it stays available for testing whenever you need a known-good sender. ## Next steps - [Send your first email](/docs/get-started/send-your-first-email) — the developer quickstart that uses the shared domain - [Email sending FAQ](/docs/knowledge-base/faq/email-sending) — common questions about sending, including the shared domain - [Test email delivery (mail sandbox)](/docs/guides/email/testing-sandbox) — deterministic bounces, complaints, and deliveries with no real inbox - [Sending domains](/docs/guides/email/sending-domains) — set up your own domain for production sending --- title: "Domain verification delays" description: "Why a sending domain can stay unverified after you've added the DNS records, and what to check while you wait." source: https://bird.com/en-us/docs/knowledge-base/dns-and-domains/verification-delays --- # Domain verification delays You added the DNS records, but your domain still shows as unverified. In almost every case nothing is wrong — DNS changes simply take time to spread, and Bird keeps checking automatically until they do. This page explains why the wait happens and gives you a short checklist for the cases where a record really does need fixing. ## DNS changes take time to propagate When you add or edit a DNS record, your DNS provider doesn't push the change out to the whole internet instantly. Resolvers around the world cache DNS answers for a period set by the record's **TTL** (time to live), and they keep serving the old answer — including "this record doesn't exist" — until that cache expires. Depending on your provider and TTL settings, this can take anywhere from a few minutes to 24–48 hours. So a domain that shows as unverified right after you've added the records usually isn't misconfigured — Bird just hasn't been able to see the new records yet. ## Bird keeps checking — your domain isn't stuck You never need to sit and refresh. Bird re-checks your domain's records automatically: checks start within seconds of registration and run frequently at first, then space out gradually over roughly the first **72 hours**. After that, every active domain is still re-checked **daily**. The moment your records resolve, the next check picks them up and the domain verifies — there is no state where a domain is permanently stuck waiting for you to do something. If you've just fixed a record and don't want to wait for the next automatic check, you can trigger one yourself: open your domain from the [**Domains**](https://bird.com/dashboard/w/email/domains) page and use the re-check action. Manual re-checks are rate-limited to a handful per hour, which is plenty for normal use. ![A domain's detail page in the Bird dashboard, listing the DNS records with their verification status](/images/docs/dashboard-email-domain-detail.png) The domain page shows each record's individual status, so you can see exactly which record is still outstanding rather than guessing. ## Checklist: when it's taking longer than expected If a record still hasn't verified after a day or so, walk through these four checks — they cover the vast majority of real problems: ### 1. Check for a duplicated domain in the host name This is the classic copy-paste mistake. Many DNS providers automatically append your domain to whatever you type in the host field. If Bird asks you to create a record for `send.example.com` and you paste the full name into a provider that appends the domain, you end up with `send.example.com.example.com` — which will never verify. If your provider appends the domain, enter only the part before it (here, just `send`). You can confirm what actually got published by looking up the record with any public DNS lookup tool. If a long DKIM value was cut off by your provider's field limit, the [DNS record splitter](/tools/dns-record-splitter) formats it into quoted chunks that fit. ### 2. Lower the TTL If your records have a high TTL (some providers default to 24 hours or more), old cached answers stick around that much longer. Lowering the TTL to something short — 5 minutes is common — makes future changes propagate much faster. It won't speed up a change you've already made (the old TTL governs that), but it helps with every correction after. ### 3. Make sure you're editing the authoritative name servers DNS records only count if they're published on the name servers that are actually **authoritative** for your domain. If your domain was recently moved between providers, or DNS is managed somewhere other than your registrar, you may be adding records in a control panel that the internet isn't reading from. Check which name servers your domain points to (a WHOIS lookup or your registrar's dashboard will show them) and make sure that's where you added the records. ### 4. Confirm all required records exist Verifying ownership and being ready to send are checked separately, and sending requires three records: - the **DKIM** TXT record, - the **return-path** CNAME record, and - a **DMARC** TXT record. The DMARC check is deliberately lenient: any valid DMARC record covering your sending domain is enough, and a record on your parent (registered) domain counts. So if `example.com` already has a DMARC record, `send.example.com` is covered — you don't need to add another one. If none exists anywhere, a minimal monitoring-only policy is all Bird requires. See [DKIM, SPF, and DMARC](/docs/guides/email/dkim-spf-dmarc) for what each record does and how to pick values. ## A note on domains that were verified and then flipped back Verification isn't a one-time event — Bird keeps re-checking verified domains daily so you find out if your DNS later breaks. To avoid false alarms from transient DNS hiccups, a verified domain is only downgraded after **two consecutive failed checks**, and it re-verifies automatically as soon as the records are back. So if a previously verified domain shows as failed, something really did change in your DNS — the duplicated-host and name-server checks above are the first places to look. ## Next steps - [Sending domains](/docs/guides/email/sending-domains) — the full developer guide to registration, records, and the verification lifecycle - [DKIM, SPF, and DMARC](/docs/guides/email/dkim-spf-dmarc) — what each record does - [Per-provider DNS walkthroughs](/docs/knowledge-base/dns-and-domains/generic-registrar) — step-by-step instructions for Cloudflare, Route 53, GoDaddy, and more --- title: "Deliverability FAQ" description: "Quick answers to recurring questions about domain verification, IP warmup, blocked recipients, and open rates." source: https://bird.com/en-us/docs/knowledge-base/faq/deliverability --- # Deliverability FAQ Quick answers to the deliverability questions that come up again and again. Each answer links to the full article for the details. ## How long does domain verification take? **Usually minutes — once your DNS records are actually visible.** The wait is almost never Bird; it's DNS propagation. Resolvers cache DNS answers, so a freshly added record can take anywhere from minutes to a day or two to be seen everywhere, depending on your provider and TTL settings. You don't need to babysit it: Bird re-checks your records automatically — frequently at first, spacing out over roughly the first 72 hours, then daily for as long as the domain is active. The moment the records resolve, the next check verifies the domain. If it's been more than a day, something probably does need fixing — the [verification delays article](/docs/knowledge-base/dns-and-domains/verification-delays) has the four-step checklist (duplicated host names are the classic culprit). ## Why can't I send to a particular recipient? Two common reasons, depending on where you are in your setup: - **The address is suppressed.** Bird automatically stops sending to addresses that have hard-bounced, unsubscribed, or complained — protecting your reputation from sends that would hurt it. Check the suppression list in your dashboard; the address will show why it's there. - **You're on the shared onboarding domain.** The shared domain only delivers to verified members of your workspace. If the recipient isn't a workspace member, the send is rejected with an error saying so — verify your own domain to send to anyone ([Email sending FAQ](/docs/knowledge-base/faq/email-sending)). ## Do I need to warm up an IP address? **Only if you have a dedicated IP — and Bird handles it for you.** New IP addresses have no sending history, so mailbox providers ramp up trust gradually; sending full volume from a cold IP gets mail throttled or junked. With a dedicated IP, Bird warms it automatically over roughly **30 days**, gradually increasing how much of your traffic it carries while the shared pool handles the rest. If you're on the shared IP pool (the default), there's nothing to warm — the pool already has established reputation. Note that your **domain** builds its own reputation too, so ramping up volume gradually on a new domain is still good practice. Details in the [warming article](/docs/knowledge-base/deliverability/warming). ## Why is my open rate so high (or so low)? Open rates have been fuzzy ever since Apple Mail Privacy Protection: Apple Mail **prefetches** tracking pixels for its users, registering "opens" for messages no human looked at. Depending on how much of your audience uses Apple Mail, this inflates open rates substantially — and it makes opens unreliable as an engagement signal in either direction. Treat opens as a rough trend line, not a truth, and lean on clicks and conversions for real engagement measurement. The [Apple Mail privacy article](/docs/knowledge-base/deliverability/apple-mail-privacy) explains the mechanics and what to measure instead. ## How do I know if my sender reputation is healthy? Watch the signals mailbox providers watch: bounce rate, complaint rate, and how your engagement trends over time. Your Metrics page shows these per domain, and external tools like Google Postmaster Tools for Gmail show how providers see you; Bird's [Email header analyzer](/tools/email-analyzer) can verify that an individual message passed SPF, DKIM, and DMARC. The [reputation monitoring article](/docs/knowledge-base/deliverability/reputation-monitoring) covers which numbers matter, healthy ranges, and where to look when one drifts. ## Next steps - [Email sending FAQ](/docs/knowledge-base/faq/email-sending) — first-send questions: shared domain, From addresses, getting started - [Domain verification delays](/docs/knowledge-base/dns-and-domains/verification-delays) — the troubleshooting checklist - [IP & domain warming](/docs/knowledge-base/deliverability/warming) — how automatic warmup works - [Reputation monitoring](/docs/knowledge-base/deliverability/reputation-monitoring) — the metrics that matter - [Apple Mail privacy & open rates](/docs/knowledge-base/deliverability/apple-mail-privacy) — why opens lie - [Sending domains](/docs/guides/email/sending-domains) — the developer guide to domains and verification --- title: "Email sending FAQ" description: "Quick answers to the most common questions about sending your first emails through Bird." source: https://bird.com/en-us/docs/knowledge-base/faq/email-sending --- # Email sending FAQ Quick answers to the questions almost everyone asks when they start sending email through Bird. Each answer links to the full article if you want the details. ## Can I send email before verifying a domain? **Yes.** Every new workspace can send immediately from Bird's shared onboarding domain — no DNS records, no verification, no waiting. There's a catch that's actually a feature: messages from the shared domain are only delivered to **verified members of your workspace** (you and your teammates), and there's a daily recipient cap. That's enough to prove your integration works end to end on day one; it's deliberately not enough for production. One exception: the mail sandbox addresses (like `delivered@` and `bounce@` at `messagebird.dev`) always work, so you can simulate deliveries and bounces without inviting anyone. See [Sending from the shared domain](/docs/knowledge-base/dns-and-domains/shared-domain) for the full rules, or jump straight to the [Send your first email](/docs/get-started/send-your-first-email) quickstart. ## What is the onboarding domain, and what are its limits? The shared onboarding domain is `messagebird.dev` — a Bird-owned domain that every workspace can send from out of the box (messages come from `onboarding@messagebird.dev`). Its guardrails, all by design: - **Recipients must be verified members of your workspace.** Sends to anyone else are rejected with a clear error. - **A daily recipient cap** — by default 50 recipients per organization per day, resetting daily. - **Sandbox addresses are exempt** from the members-only rule, but still count toward the cap. It never goes away: even after you verify your own domain, the shared domain stays available as a known-good test sender. Full details in [Sending from the shared domain](/docs/knowledge-base/dns-and-domains/shared-domain). ## Can I use a Gmail, Yahoo, or other free-email address as my From address? **No.** You can only send from domains you've verified in Bird, and you can't verify `gmail.com` — you don't own it. Even setting that aside, it wouldn't work: free-mail providers publish strict DMARC policies, so mail claiming to be from `you@gmail.com` but sent through Bird would fail authentication and be rejected or junked by recipients. Gmail and Yahoo's own bulk-sender rules require exactly this kind of authentication. The fix is to send from a domain you own — `you@yourcompany.com` — which you verify once and control completely. See [Gmail & Yahoo sender requirements](/docs/knowledge-base/deliverability/gmail-yahoo-requirements) for why the rules exist, and the [Sending domains guide](/docs/guides/email/sending-domains) for how to set your domain up. ## How do I set up my own sending domain? Register the domain in Bird, publish the DNS records it gives you (DKIM, return-path, and DMARC), and wait for verification — usually minutes once the DNS records are live. The [Sending domains guide](/docs/guides/email/sending-domains) walks through the whole lifecycle, and there are [step-by-step walkthroughs per DNS provider](/docs/knowledge-base/dns-and-domains/generic-registrar) if you want exact clicks for Cloudflare, GoDaddy, Route 53, and others. ## Where do I start as a developer? The [Send your first email](/docs/get-started/send-your-first-email) quickstart takes you from API key to a delivered message in a few minutes, using the shared onboarding domain so there's nothing to configure first. ## Next steps - [Deliverability FAQ](/docs/knowledge-base/faq/deliverability) — verification times, warmup, suppressions, open rates - [Sending from the shared domain](/docs/knowledge-base/dns-and-domains/shared-domain) — the onboarding domain in full - [Sending domains](/docs/guides/email/sending-domains) — the developer guide to your own domain --- title: "Create an account & workspace" description: "Sign up for Bird, verify your email address, and get to know the workspace you land in." source: https://bird.com/en-us/docs/knowledge-base/getting-started/create-account --- # Create an account & workspace Signing up for Bird takes a few minutes: enter your details, confirm your email address, and you land in a ready-to-use workspace. This page walks through each step and explains what the account you end up with actually contains. ## 1. Sign up Go to the Bird signup page and enter your work email address and a password. Use the address you check regularly — Bird sends a verification email there, and it becomes the address your teammates will recognize when you start [inviting them](/docs/knowledge-base/getting-started/invite-your-team). ## 2. Verify your email address Right after signup, Bird emails you a verification code. Open the message, copy the code, and enter it on the confirmation screen (or click the confirmation link in the email — either works). Until you confirm, your account exists but can't do much, so if the email hasn't arrived within a minute or two, check your spam folder or use the **Resend** option on the confirmation screen. Once your address is confirmed, you're signed in and looking at your dashboard for the first time. ## 3. Meet your workspace You land in a **workspace** — where the work happens. Sending, domains, message history, suppressions, webhooks, API keys, and your team all live here, and the dashboard is built around it. Billing, API keys, and settings are reachable from the workspace menu — the dropdown next to your workspace name. One practical consequence worth knowing even if you never touch the API yourself: an **API key belongs to exactly one workspace**, so a key can only send and read data in the workspace it was created in. Today you have a single workspace; if your team grows you might add more later to separate things like production from testing. The [dashboard tour](/docs/knowledge-base/getting-started/dashboard-tour) shows where everything lives. ## 4. Send a test email — no setup required You don't need to configure anything to see Bird work. Every new workspace can send from Bird's **shared onboarding domain** immediately — no DNS records, no domain verification, nothing to wait for. It's meant exactly for this moment: proving the pipes work before you set up your own domain. - Curious how the shared domain works and what its limits are? See the [Email sending FAQ](/docs/knowledge-base/faq/email-sending). - Ready to send one (you or a developer on your team)? The [Send your first email quickstart](/docs/get-started/send-your-first-email) is the fastest path from a fresh account to a delivered message. ## Next steps - Take the [dashboard tour](/docs/knowledge-base/getting-started/dashboard-tour) to learn where everything lives. - [Invite your team](/docs/knowledge-base/getting-started/invite-your-team) so the right people have access from day one. --- title: "Dashboard tour" description: "How the Bird dashboard is put together — the app switcher, what every channel app shares, the workspace-level pages, and the controls that follow you everywhere." source: https://bird.com/en-us/docs/knowledge-base/getting-started/dashboard-tour --- # Dashboard tour This page is a map, not a manual: how the Bird dashboard is put together, so you know where everything lives before you start clicking. If you've just [created your account](/docs/knowledge-base/getting-started/create-account), start here. The shape is simple. You're always in a **workspace**, named in the breadcrumb at the top left. The **app switcher** next to it moves you between the workspace's apps: one per channel, plus the developer, billing, and settings pages. A few controls — search, help, your profile — sit in the top bar wherever you are. That's the whole model; the rest of this page walks each part. ## Where you'll spend your first session Three stops cover the setup work for a new account: 1. **Your channel's setup step.** Every channel starts by proving who you are on it — registering the identity you'll send from. Each channel's overview walks its own setup: [Email](/docs/guides/email/overview). 2. **Developers → [API keys](https://bird.com/dashboard/w/api-keys)** — create a key for whoever is connecting your product or tools to Bird. Each key belongs to the workspace it was created in. 3. **Settings → [Team](https://bird.com/dashboard/w/settings/team)** — get the right people in, each with a role that matches what they should be able to do. See [Invite your team](/docs/knowledge-base/getting-started/invite-your-team). ![The API Keys page in the Bird dashboard, listing keys with their masked prefix, scopes, and last-used time](/images/docs/dashboard-api-keys.png) Everything else can wait until you're actually sending. ## The app switcher The menu next to your workspace name is how you move around. It lists **Home**, one app per channel — **Email**, **SMS**, **WhatsApp**, **Voice**, **Push**, **Verify**, **Lookup**, and **Realtime** — and then the workspace-level pages: **Developers**, **Billing and payments**, and **Settings**. ![The app switcher menu open in the Bird dashboard, listing Home, the channel apps (Email, SMS, WhatsApp, Voice, Push, Verify, Lookup, Realtime), the Developers pages, Billing and payments, and Settings](/images/docs/dashboard-workspace-menu.png) ## Channel apps Every channel opens as its own app, and they all share one anatomy: a sub-navigation of pages scoped to that channel — its activity log, its metrics, and its configuration. Learn one and the rest will feel familiar. Each channel's overview documents its app page by page: [Email](/docs/guides/email/overview#the-email-app-in-the-dashboard). ## Developers, billing, and settings Three stops in the app switcher sit outside the channel apps and apply to the whole workspace: - **Developers** — [**API keys**](https://bird.com/dashboard/w/api-keys) (create and revoke the keys that let software use this workspace — see [API keys](/docs/knowledge-base/account-security/api-keys)), [**Webhooks**](https://bird.com/dashboard/w/webhooks) (endpoints where Bird notifies your systems about events as they happen — see [Webhooks](/docs/guides/webhooks)), and [**Logs**](https://bird.com/dashboard/w/logs). - **Billing and payments** — your plan and payment details, split across tabs: **Wallet**, **Subscriptions** (current plan and upgrades), **Usage**, **Payment Methods**, **Invoices**, **Transaction History**, and **Billing Address**. - **Settings** — three tabs: [**Workspace**](https://bird.com/dashboard/w/settings) (general details — name, ID, created date), [**Notifications**](https://bird.com/dashboard/w/settings/notifications), and [**Team**](https://bird.com/dashboard/w/settings/team) (who has access and with what role, plus an **Invite members** button). ![The Team tab of the Settings page in the Bird dashboard: the members list with roles and the Invite members button](/images/docs/dashboard-settings-team.png) ## Search and help Two controls live in the top right on every page: - **Search (⌘K)** opens the command palette: type to jump to any page or product in the workspace, or use the two-key go-to shortcuts it shows next to each destination. - **Need help? (H)** opens Bird's support chat, where you can ask a question, contact support, share feedback, and pick up past conversations. ## Your profile The avatar in the top-right corner is you, not the workspace. Its menu re-opens the **Get started** checklist, switches **Appearance** (light, dark, or system), and logs you out. **Account settings** opens your personal profile: account details, [**Security**](https://bird.com/dashboard/profile/security) (password and [multi-factor authentication](/docs/knowledge-base/account-security/login-mfa)), [**Sessions**](https://bird.com/dashboard/profile/sessions) (everywhere you're signed in), and [**Connected apps**](https://bird.com/dashboard/profile/connected-apps) (OAuth grants you've approved, like the [MCP server](/docs/ai/mcp-server)). These follow your user account across every workspace you belong to. ![The profile menu open in the Bird dashboard: Get started, Account settings, Appearance, and Log out under the signed-in user's email](/images/docs/dashboard-profile-menu.png) ## Next steps - Set up your channel end to end: [Email overview](/docs/guides/email/overview) - Get the right people in: [Invite your team](/docs/knowledge-base/getting-started/invite-your-team) --- title: "Invite your team" description: "Add teammates to your Bird workspace, choose the right role for each person, and send an invitation." source: https://bird.com/en-us/docs/knowledge-base/getting-started/invite-your-team --- # Invite your team Bird is built for teams: each person signs in with their own account and gets exactly the access their role describes. This page shows how to send an invitation, what happens on the other end, and how to pick the right access level. ## Pick a role You invite people to your **workspace** with a role that sets exactly what they can do. There are three: - **admin** — runs the workspace, including its team and settings. - **developer** — builds the integration: sends, domains, webhooks, API keys. - **analyst** — read-only: can look at everything, change nothing. Most teammates only need one of these. (Behind the workspace, an **owner** role manages billing and the member list across the account; you hold it automatically as the person who created the account, and it's wise to have a second owner so no one person is a bottleneck. It rarely comes up day to day — see [Users, teams & roles](/docs/guides/users-teams-roles).) One subtlety worth knowing: workspace access also controls who can be sent test messages from Bird's [shared onboarding domain](/docs/knowledge-base/dns-and-domains/shared-domain), so a teammate who should receive test sends during setup needs access to the workspace doing the sending. ## Send an invitation In the dashboard, open [**Settings → Team**](https://bird.com/dashboard/w/settings/team). You'll see everyone who currently has access to the workspace, each with their role, plus any invitations that haven't been accepted yet. ![The workspace Team settings in the Bird dashboard, listing members with their role and the invite-members action](/images/docs/dashboard-settings-team.png) Click **Invite members**, enter your teammate's email address, and choose their role. What happens next depends on whether they already use Bird: - **New to Bird** — they receive an invitation email with a signup link. Once they create their account, they land directly in your workspace with the role you chose. The link is valid for **7 days**; after that the invitation expires and you'll need to send a new one from the same screen. - **Already in your organization** — they're added to the workspace immediately with the chosen role. No email, no waiting. You can withdraw a pending invitation at any time from the Team screen, and inviting the same address twice won't create a duplicate — you'll be pointed at the invitation that's already pending. ## Accepting an invitation If you're on the receiving end: the invitation arrives by email from Bird. Click the link, create your account (or sign in if you already have one), and you're in — the workspace appears in your dashboard with the access your inviter chose. If the link has expired, ask the person who invited you to re-send it. ## Changing roles and removing people Roles aren't permanent: a workspace admin or an owner can change anyone's role or remove them from the **Settings → Team** screen as your team evolves. Two guardrails apply — you can't change your own access (someone else has to do it), and an organization can never lose its last owner. For the full picture of what each role can and can't do, see [Users, teams & roles](/docs/guides/users-teams-roles). ## Next steps - [Users, teams & roles](/docs/guides/users-teams-roles) — the full permission matrix behind each role. - Take the [dashboard tour](/docs/knowledge-base/getting-started/dashboard-tour) so new teammates know where everything lives. - [SSO & provisioning](/docs/knowledge-base/account-security/sso) — let your team sign in through your identity provider. --- title: "Acceptable use policy" description: "A plain-language summary of what you can't send through Bird, and what happens when sending crosses the line." source: https://bird.com/en-us/docs/knowledge-base/policies/acceptable-use --- # Acceptable use policy This article is a plain-language summary of how Bird may and may not be used. It exists so you can answer "is this okay to send?" without reading legal text. **It is a summary, not the contract** — the binding rules are in Bird's formal terms and its [Acceptable Use Policy](https://bird.com/legal/acceptable-use-policy), and where this article and the formal text differ, the formal text wins. The short version: send messages that people asked for, that are honest about who they're from and what they contain, and that are legal where you and your recipients are. Almost everything below is a specific case of breaking one of those three rules. ## What's prohibited **Spam and unsolicited bulk messaging.** Sending marketing or bulk messages to people who didn't agree to receive them — including purchased, rented, or scraped contact lists. Consent is the foundation of everything sent through Bird; the [consent article](/docs/knowledge-base/compliance/email/consent) explains what counts and how to keep records. **Phishing and fraud.** Impersonating another person, brand, or organization; spoofing sender identities; misleading subject lines or content designed to trick recipients into revealing credentials, payment details, or personal information; and any other deceptive scheme. **Malware and malicious content.** Distributing viruses, ransomware, spyware, or links to malicious sites — whether in the message body, attachments, or behind shorteners and redirects. **Harassment and abuse.** Messages that threaten, intimidate, or harass individuals or groups, including continuing to message people who have opted out or asked you to stop. **Illegal content and activity.** Content that is illegal to send or promote in your jurisdiction or your recipients' — and using Bird to facilitate illegal activity of any kind. Some content categories are also restricted in specific countries or channels even when legal elsewhere; see [Supported countries & restrictions](/docs/knowledge-base/policies/supported-countries). ## How Bird enforces this Bird continuously monitors trust-and-safety signals across the platform — complaint rates, bounce patterns, recipient feedback, content signals, and reports from mailbox providers and carriers. This protects every sender: shared infrastructure means one bad actor can damage deliverability for everyone, so abuse is caught and contained quickly. When sending crosses the line, the response is proportionate to the severity: - **Throttling** — your sending rate is reduced while signals are elevated, giving you time to correct course. - **Pausing** — sending is suspended on the affected domain or workspace until the issue is resolved. - **Account action** — for serious or repeated violations, Bird can suspend or terminate the account. If your sending is throttled or paused, you'll see it in the dashboard along with the reason. The [abuse & compliance overview](/docs/guides/abuse-and-compliance) explains how the enforcement pipeline works per channel. ## How to stay on the right side In practice, compliant senders all do the same few things: collect real consent before messaging, keep records of it, honor opt-outs immediately, and send content that matches what people signed up for. The compliance articles walk through exactly how: - [Consent & data privacy (GDPR/CCPA)](/docs/knowledge-base/compliance/consent-and-privacy) — the cross-channel basics - [Email list consent & CAN-SPAM](/docs/knowledge-base/compliance/email/consent) — the email-specific rules ## Next steps - [Supported countries & restrictions](/docs/knowledge-base/policies/supported-countries) — where Bird can and can't deliver, and content restricted by market - [Abuse & compliance](/docs/guides/abuse-and-compliance) — how enforcement works across channels - [Bird's Acceptable Use Policy](https://bird.com/legal/acceptable-use-policy) — the binding legal text this article summarizes --- title: "Supported countries & restrictions" description: "Which markets Bird can serve, where sending is restricted by sanctions or local law, and how data residency regions work." source: https://bird.com/en-us/docs/knowledge-base/policies/supported-countries --- # Supported countries & restrictions Before launching in a new market, two questions matter: **can Bird serve recipients there at all**, and **is your content allowed there**? This article explains how both kinds of restriction work and where to find the current, authoritative answers. It's a reference for planning a launch — not a substitute for your own legal review of the markets you operate in. ## Where Bird can and can't serve Bird delivers to recipients in most of the world, but some regions cannot be served at all: - **Sanctioned regions.** Bird is subject to international sanctions regimes (including those of the US and EU), which prohibit providing services to certain countries, regions, and individuals. Sending to sanctioned destinations is blocked at the platform level — this isn't a setting you can change. - **Legally restricted regions.** A small number of additional markets are restricted because local law makes compliant delivery impossible or because Bird has no lawful route to recipients there. Sanctions lists change, and so does the set of restricted markets. **Don't rely on a snapshot** — before committing to a new market, confirm the current status with Bird support or your account team, who maintain the authoritative list. ## Content restricted by market Even in fully supported countries, some content categories are restricted or prohibited — by local law, by carrier and mailbox-provider rules, or by Bird's own policy. Common examples of categories that are regulated differently per market: - Gambling and betting - Alcohol, tobacco, and cannabis - Pharmaceuticals and health claims - Financial services, credit offers, and cryptocurrency - Adult content - Political and religious messaging A category that's fine in one country may require licensing in a second and be flat-out prohibited in a third. Content that violates [Bird's Acceptable Use Policy](https://bird.com/legal/acceptable-use-policy) — spam, phishing, malware, harassment, illegal content — is prohibited everywhere, regardless of market. If your business operates in a regulated category, raise it with Bird support before launching: it's far cheaper to confirm up front than to have sending paused mid-campaign. ## Data residency: pick your region up front Where Bird _delivers_ is separate from where your _data lives_. Every Bird organization is pinned to one data residency region at signup — `us1` (United States) or `eu1` (European Union) — and all of its data is stored and processed in that region, never replicated across regions. You can send to recipients worldwide from either region; residency governs your data, not your audience. The assignment is immutable, so choose deliberately: if your compliance posture requires EU residency (common for GDPR-driven processing), sign up in `eu1`. See [Consent & data privacy](/docs/knowledge-base/compliance/consent-and-privacy) for what residency means for your privacy obligations, and [Base URLs & regions](/docs/api/regions) for how the region model works at the API level. ## Launch checklist for a new market 1. Confirm the destination isn't sanctioned or restricted — ask Bird support for the current list. 2. Check whether your content category is regulated in that market. 3. Review the consent rules that apply to recipients there ([consent & privacy](/docs/knowledge-base/compliance/consent-and-privacy)). 4. Make sure your sending complies with the [Acceptable Use Policy](https://bird.com/legal/acceptable-use-policy) — it applies in every market. ## Next steps - [Acceptable use summary](/docs/knowledge-base/policies/acceptable-use) — plain-language guide to what can't be sent anywhere - [Consent & data privacy (GDPR/CCPA)](/docs/knowledge-base/compliance/consent-and-privacy) — consent and residency obligations - [Base URLs & regions](/docs/api/regions) — the `us1`/`eu1` region model for developers --- title: "Status page & incidents" description: "How to read Bird's status page, subscribe to incident updates, and tell a platform incident apart from a problem with your own account." source: https://bird.com/en-us/docs/knowledge-base/status/status-and-incidents --- # Status page & incidents When something seems wrong with your sending, the first question is always the same: **is it Bird, or is it me?** Bird's status page answers the first half. This article explains how to read it, what to expect during an incident, and how to work out whether a problem is platform-wide or specific to your account or domain. ## Reading the status page Bird's status page shows the current health of each platform component — the API, the dashboard, and delivery per channel and region — so you can check the piece you actually depend on rather than a single overall light. Each component shows one of a few states: - **Operational** — working normally. - **Degraded performance** — working, but slower than usual. Sends are accepted; delivery may lag. - **Partial outage** — some requests or some regions affected. - **Major outage** — the component is down for most or all users. Active incidents appear at the top of the page with a severity, the affected components, and a running timeline of updates. Past incidents stay visible in the history, each ending with a resolution note. ## Subscribing to incident notifications You don't need to poll the page. The status page lets you subscribe to notifications — email and feed-based options are available — so you're told when an incident opens, when there's a material update, and when it's resolved. If Bird's availability matters to your own operations, subscribe your on-call channel rather than one person's inbox. ## What an incident means for your sending Here's the part that surprises people in a good way: **for email, a platform incident usually means delayed delivery, not lost mail.** Email is store-and-forward by design — when Bird accepts a send (your API call returns success), the message is durably queued. If delivery is degraded, accepted messages wait in the queue and go out when the incident resolves. You generally don't need to detect the incident and re-send; re-sending typically just produces duplicates after recovery. What you may see during an incident, depending on which component is affected: - **Delivery degraded** — sends are accepted normally, but messages reach inboxes later than usual. Queued mail drains automatically after resolution. - **API degraded or down** — new send requests may be slow or fail. Failed API calls were _not_ accepted, so those are the ones to retry (with backoff). - **Dashboard degraded** — sending is unaffected; you just can't see it. The dashboard is a window onto the platform, not part of the delivery path. ## How Bird communicates during an incident Incidents follow a consistent rhythm: an initial post when the problem is confirmed (what's affected, what's known), updates as investigation and mitigation progress, and a resolution note when service is restored. For significant incidents, a post-incident review follows with the cause and what's being done to prevent recurrence. The timeline on the status page is the single source of truth — support will point you to the same incident entry. ## Platform problem or just you? Most delivery problems are _not_ platform incidents — they're account-side issues like an unverified domain or suppressed recipients. Work through this order: 1. **Check the status page.** If there's an active incident covering your channel and region, that's your answer: expect delays, don't re-send accepted messages, and watch the incident for updates. 2. **Check your domain's verification status.** A domain whose DNS records broke stops sending regardless of platform health — the domain's page in the dashboard shows each record's status. ![A domain's DNS Records page in the Bird dashboard, showing the verified DKIM record with copyable name and value, and the return-path and DMARC sections below](/images/docs/dashboard-email-domain-detail.png) 3. **Check your Metrics page.** If sends are being accepted but bouncing or being deferred, the metrics and event logs show the pattern — and whether it's one mailbox provider, one domain, or everything. If the status page is green and your own checks come up clean, start with the [delivery delays troubleshooting article](/docs/knowledge-base/troubleshooting/delays) — it walks through the account-side causes in order of likelihood. If your sending has been deliberately slowed or stopped, see [throttled or paused sending](/docs/knowledge-base/troubleshooting/throttled-paused) instead. ## Next steps - [Delivery delays](/docs/knowledge-base/troubleshooting/delays) — the account-side checks to run when mail is slow - [Throttled or paused sending](/docs/knowledge-base/troubleshooting/throttled-paused) — what it means when Bird slows or stops your sending --- title: "Why did my email bounce?" description: "What hard and soft bounces mean, what Bird does about each automatically, and where to find the exact reason a recipient's mail server gave." source: https://bird.com/en-us/docs/knowledge-base/troubleshooting/bounces --- # Why did my email bounce? A bounce means the recipient's mail server didn't accept your message. That sounds alarming, but bounces are a normal part of email — what matters is which kind you got, because the two kinds mean very different things and Bird handles each one for you automatically. ## Hard bounces: the address doesn't work A **hard bounce** is a permanent failure. The receiving server said, definitively, that it will never accept mail for this address — usually because the address doesn't exist (a typo at signup, an employee who left, a mailbox that was deleted) or because the domain itself can't receive mail. Because retrying a hard bounce is pointless, Bird doesn't. Instead, the address is **automatically added to your suppression list**, and any future send to it is rejected before it ever leaves Bird. This protects you: repeatedly mailing dead addresses is one of the strongest signals mailbox providers use to mark a sender as untrustworthy, so stopping those sends keeps your reputation intact. One thing Bird can't do for you: **remove the address from your own list**. If your mailing list, CRM, or database still contains hard-bounced addresses, clean them out — keeping them around inflates your contact counts and, if you ever sync your list to another tool, risks mailing them again from somewhere else. The [list hygiene guide](/docs/knowledge-base/deliverability/list-hygiene) covers how to keep your list clean over time. ## Soft bounces: a temporary problem A **soft bounce** (also called a deferral) is a temporary failure. The receiving server couldn't take the message right now — the mailbox is full, the server is busy or briefly unavailable, or the provider is slowing down incoming mail — but it may well accept it later. Bird **retries soft bounces automatically**. Most resolve on their own: the message is delivered on a later attempt and you never need to do anything. Only if the retries keep failing over an extended period does Bird give up and record the recipient as bounced. Soft bounces do **not** add the address to your suppression list, because the address itself is fine. If you're seeing a delay rather than a failure, that's usually this retry cycle in action — see [Delivery delays & retries](/docs/knowledge-base/troubleshooting/delays). ## Where to see why a specific recipient bounced Every message in Bird tracks its recipients individually, so one send to many people can have some delivered and some bounced — and you can see exactly which is which. Open the message's detail view in the dashboard. It lists each recipient with their outcome, and for bounced recipients it shows the **reason the receiving server gave** — the actual response from the other side, like "user unknown" or "mailbox full". That reason tells you whether you're looking at a dead address (hard) or a passing problem (soft), and it's the same information Bird used to decide whether to suppress or retry. ![The detail panel of a bounced message in the Bird dashboard, opened over the email log filtered to bounced messages: the Events tab shows Accepted and Processed followed by a highlighted Bounced event with the receiving server's reason — "smtp; 550 5.1.1 The email account that you tried to reach does not exist" — and a notice that the address was auto-added to the suppression list](/images/docs/dashboard-email-detail-bounced.png) ## What to do about bounces - **Hard bounces**: nothing to fix on Bird's side — the address is already suppressed. Remove it from your own list so it doesn't come back. - **Soft bounces**: usually nothing at all — Bird is retrying. If the same addresses soft-bounce on every campaign, the mailboxes may be abandoned and worth removing anyway. - **Lots of hard bounces at once**: that's a list-quality problem, not a one-off. A high bounce rate can slow or pause your sending — see [Why was I throttled or paused?](/docs/knowledge-base/troubleshooting/throttled-paused) — so pause the campaign and clean the list first. ## Next steps - [Delivery delays & retries](/docs/knowledge-base/troubleshooting/delays) — what happens during the retry cycle and what "delivered" really means - [Why was I throttled or paused?](/docs/knowledge-base/troubleshooting/throttled-paused) — what sustained high bounce rates trigger - [List hygiene](/docs/knowledge-base/deliverability/list-hygiene) — keeping bounce rates low in the first place - [Suppressions](/docs/guides/email/suppressions) — the developer guide to how the suppression list works and how to manage it --- title: "Common error messages" description: "A plain-language glossary of the API errors people hit most often — what each one means, the likely cause, and the fix." source: https://bird.com/en-us/docs/knowledge-base/troubleshooting/common-errors --- # Common error messages When a request to Bird fails, you get back a structured error: a stable error code you can match on, a human-readable message, and a link to the documentation for that exact error. So the error itself is always your first clue. This page translates the errors people hit most often into plain language — what each means, why it usually happens, and how to fix it. For the complete catalog with every code, see the [API error reference](/docs/api/errors). ## Validation errors These mean Bird understood your request but something in it isn't acceptable. The error lists the specific field or condition at fault. ### All recipients suppressed **What it means**: every recipient on your send is on your suppression list, so there was nothing left to deliver and the send was refused. **Likely cause**: you're sending to addresses that previously hard-bounced, complained, or unsubscribed — often a sign you're re-mailing an old or uncleaned list. If only _some_ recipients are suppressed, the send goes through for the rest and the suppressed ones show as rejected; this error only appears when it's all of them. **The fix**: check which addresses are suppressed and why, and remove them from your own list. [Why was my email rejected?](/docs/knowledge-base/troubleshooting/rejections) explains how suppression rejections show up, and the [suppressions guide](/docs/guides/email/suppressions) covers managing the list. ### Onboarding recipient not allowed **What it means**: you're sending from Bird's shared onboarding domain to someone who isn't a verified member of your workspace. **Likely cause**: the shared domain is a test-and-first-send capability — it only delivers to you and your teammates (plus the sandbox test addresses), by design, to protect the domain's reputation for everyone sharing it. **The fix**: to email real recipients, verify your own sending domain — that removes the restriction entirely. See [Sending from the shared domain](/docs/knowledge-base/dns-and-domains/shared-domain) for the guardrails and the way out. ### Missing or invalid field **What it means**: a required field is absent, or a value doesn't validate — a malformed address, an empty subject, a field that isn't supported yet. **Likely cause**: usually a small mistake in the request your code builds. The error's details name each failing field and what's wrong with it. **The fix**: read the field list in the error and correct the named fields — no guessing needed. ## Rate-limit errors **What it means**: you've made more requests in the current window than your plan allows. This is temporary by definition. **Likely cause**: either a burst of send requests exceeded your per-minute limit, or — if you're on the shared onboarding domain — you've hit its **daily recipient cap** (by default 50 recipients per organization per day, resetting the next day). **The fix**: wait and retry — the error tells you how long. For per-minute limits, the wait is seconds; spreading requests out or batching sends avoids it entirely, and higher plan tiers raise the ceiling. For the onboarding daily cap, the real fix is verifying your own domain, which removes the cap. If sending is being slowed for reputation reasons rather than request volume, that's a different mechanism — see [Why was I throttled or paused?](/docs/knowledge-base/troubleshooting/throttled-paused). ## Authentication errors **What it means**: Bird couldn't accept your credentials. **Likely cause**: one of three things, in rough order of frequency — - **Wrong or revoked API key**: the key is mistyped, truncated, or was revoked (keys revoked by a teammate are a classic). Keys are only shown in full once, at creation. - **Key used against the wrong region**: API keys are regional, and a key only works against its own region's servers. If your key was created in one region and your code calls another, authentication fails — the key prefix tells you which region it belongs to. - **Missing key**: the request didn't include credentials at all, often an environment variable that's empty in the environment that's failing. **The fix**: confirm the key exists and is active in your dashboard, that your code is sending it, and that you're calling the regional address that matches the key. If in doubt, create a fresh key and swap it in. ![The API Keys page in the Bird dashboard, listing keys with their masked prefix, scopes, and last-used time](/images/docs/dashboard-api-keys.png) ## Domain not verified **What it means**: you tried to send from a domain that hasn't completed verification, so Bird can't send as it yet. **Likely cause**: the domain's DNS records aren't published yet, haven't propagated, or have a small mistake — or the domain was verified before and its DNS has since changed. DNS propagation alone can take from minutes up to a day or two. **The fix**: open the domain's page in the dashboard to see exactly which record is outstanding. [Domain verification delays](/docs/knowledge-base/dns-and-domains/verification-delays) has the checklist for the cases where a record genuinely needs fixing. While you wait, the [shared onboarding domain](/docs/knowledge-base/dns-and-domains/shared-domain) keeps test sends working. ## Reading any error you meet Every Bird error carries the same three things: a **stable code** (safe to match on in your code — it never changes), a **human message** (display or log it, but don't parse it), and a **docs link** straight to the page for that error. It also includes a request ID — quote that in any support conversation and we can find the exact request. ## Next steps - [API error reference](/docs/api/errors) — the full catalog: every error type, code, and status - [Why was my email rejected?](/docs/knowledge-base/troubleshooting/rejections) — the reasons behind per-recipient rejections - [Why was I throttled or paused?](/docs/knowledge-base/troubleshooting/throttled-paused) — reputation-based slowdowns and pauses - [Sending from the shared domain](/docs/knowledge-base/dns-and-domains/shared-domain) — the onboarding domain's recipient and daily-cap guardrails --- title: "Delivery delays & retries" description: "Why an email can take a while to arrive, what deferred and delivered actually mean, and where to watch a message work its way through delivery." source: https://bird.com/en-us/docs/knowledge-base/troubleshooting/delays --- # Delivery delays & retries You sent a message and it hasn't arrived yet. Before assuming something failed: most delays are the receiving server asking Bird to come back later, and Bird doing exactly that. This page explains what happens between "sent" and "delivered", and where to watch it happen. ## How delivery actually works When Bird sends your email, it hands the message to the **recipient's mail server** — Gmail, Outlook, a company mail server — and that server decides what to do with it. Three things can happen: - It **accepts** the message. That's a delivery: the receiving server has taken responsibility for getting it to the mailbox. - It **defers** the message — "try again later". The mailbox might be full, the server busy, or the provider deliberately slowing down mail from senders it doesn't know well yet (a common, harmless practice called greylisting). This is a soft bounce, and it's temporary by definition. - It **refuses** the message permanently. That's a hard bounce — covered in [Why did my email bounce?](/docs/knowledge-base/troubleshooting/bounces). The important one for delays is the middle case: **deferrals are normal**, and Bird **retries them automatically** on a schedule. Most deferred messages are accepted on a later attempt without you doing anything. A message can defer more than once before it lands — each retry is just Bird patiently knocking again. ## What the statuses mean As a message moves through this process, each recipient's status tells you where things stand: - **Accepted / queued** — Bird has your message and is preparing it for delivery. This is the starting state of every send. - **Deferred / retrying** — Bird attempted delivery and the receiving server asked it to try again later. Not a failure: Bird is retrying, and the recipient will end up either delivered or bounced. - **Delivered** — the receiving server accepted the message and took responsibility for it. One nuance about **delivered**: it means the recipient's mail server accepted the message, which is as far as any sender can see. Where the message lands inside the mailbox — inbox, promotions tab, spam folder — is the receiving provider's decision and isn't reported back. If messages are delivering but not being seen, that's a deliverability question (sender reputation, authentication, content), not a delivery delay. ## A delay is not a failure The key reassurance: a message sitting in deferred/retrying has not failed and doesn't need to be resent. Resending while Bird is still retrying just risks the recipient getting it twice. Only if every retry is exhausted does the recipient end as bounced — and at that point you'll see it clearly, with the receiving server's reason attached. Transient delays of minutes — occasionally longer when a receiving server is greylisting or having a bad day — are a normal part of email between any two servers on the internet. ## Where to watch a message's progress Open the message's detail view in the dashboard. Each recipient has its own **timeline of events**: when the message was accepted, when it was queued, each delivery attempt and deferral with the receiving server's response, and the final delivery. So instead of wondering whether anything is happening, you can watch the retry cycle directly and see exactly what the other side said each time. ![The message detail panel in the Bird dashboard: the Events tab showing the delivery timeline (Accepted, Processed, Delivered) with timestamps, over the dimmed email list](/images/docs/dashboard-email-detail.png) If you'd rather be notified than watch, your systems can receive these same per-recipient events as webhooks — the [events developer guide](/docs/guides/email/events) documents the full event vocabulary, including the deferred and delivered events this page describes. ## Next steps - [Why did my email bounce?](/docs/knowledge-base/troubleshooting/bounces) — what happens when retries run out, and hard vs soft bounces - [Email events reference](/docs/guides/email/events) — the developer guide to every event in a recipient's timeline --- title: "Why was my email rejected?" description: "The common reasons Bird refuses a message or recipient before delivery is attempted: suppressed recipients, invalid addresses, rate limits, and policy." source: https://bird.com/en-us/docs/knowledge-base/troubleshooting/rejections --- # Why was my email rejected? A rejection is different from a bounce. A **bounce** is the receiving server saying no after Bird tried to deliver; a **rejection** means the message (or one of its recipients) never got that far — Bird stopped it before or during sending. Rejections always come with a reason you can inspect, so this page walks through the common ones and what to do about each. ## The recipient is on your suppression list The most common rejection by far. If an address previously hard-bounced, reported your mail as spam, unsubscribed, or was added to the suppression list manually, Bird won't deliver to it — that's the suppression list doing its job of protecting your sender reputation. Two things worth knowing about how this shows up: - **Suppressed recipients are not silently dropped.** They still appear in the message's recipient list, marked as rejected, with a reason you can inspect — so you can always see exactly which addresses were blocked and why. The rest of the recipients are unaffected and deliver normally. - **If every recipient of a send is suppressed**, there's nothing left to deliver, so the whole send is refused with an **all recipients suppressed** error and no message is created. If you believe an address shouldn't be suppressed, you can review and remove entries — the [suppressions guide](/docs/guides/email/suppressions) explains the different suppression reasons and how to manage the list. Be careful removing hard-bounce entries: if the address still doesn't exist, the next send will just bounce and re-suppress it. ## The address is invalid If an address is malformed, or it's on a placeholder domain that can't receive mail (like `example.com` or a test domain), Bird rejects it rather than attempting a delivery that's guaranteed to fail. The fix is at the source: validate addresses when you collect them, so typos never make it into your list. ## You hit a rate limit Bird limits how many send requests you can make per minute, based on your plan. If you exceed it, requests are refused until the window resets — the response tells you how long to wait. This is temporary by design: wait a moment and retry, or if you're regularly bumping into the limit, batching your sends or moving up a tier raises the ceiling. Rate-limit errors are covered in more detail in [Common error messages](/docs/knowledge-base/troubleshooting/common-errors). ## A policy block Sends can be refused by sending policy — for example, sending from the shared onboarding domain to someone who isn't a verified member of your workspace, or sending from a domain that isn't verified yet. Policy rejections always name what was blocked and why, so the error message itself points at the fix. If your sending has been paused for reputation reasons (high bounce or complaint rates), sends are also refused — that's a different situation with its own recovery path, covered in [Why was I throttled or paused?](/docs/knowledge-base/troubleshooting/throttled-paused). ## A content or generation error If the message itself couldn't be built — a template problem, broken content, a field that didn't validate — the recipient is rejected with a generation or validation error. These are problems with the send, not with the recipient's address, so they never affect the recipient's standing: fix the content and resend. ## How to find out which one happened Open the message's detail view in the dashboard: each recipient shows its outcome, and rejected recipients carry the specific rejection reason. If the whole request was refused (rather than individual recipients), the error response names the exact problem — the [Common error messages](/docs/knowledge-base/troubleshooting/common-errors) glossary translates the ones people hit most often. ![The detail panel of a failed message in the Bird dashboard: the Events tab shows the per-recipient outcome — here a Bounced event carrying the receiving server's exact response, "smtp; 550 5.1.1 The email account that you tried to reach does not exist" — with the email log dimmed behind it](/images/docs/dashboard-email-detail-bounced.png) ## Next steps - [Common error messages](/docs/knowledge-base/troubleshooting/common-errors) — plain-language explanations of the errors behind rejections - [Why was I throttled or paused?](/docs/knowledge-base/troubleshooting/throttled-paused) — when sending is refused for reputation reasons - [Suppressions](/docs/guides/email/suppressions) — the developer guide to the suppression list and how rejected recipients are reported --- title: "Why was I throttled or paused?" description: "Why Bird slows or pauses sending when bounce, complaint, or unsubscribe rates climb, what you see when it happens, and the path back to full speed." source: https://bird.com/en-us/docs/knowledge-base/troubleshooting/throttled-paused --- # Why was I throttled or paused? If your sends are suddenly going out slower than usual, or being refused entirely, Bird's protective limits have likely kicked in. This isn't a punishment — it's a circuit breaker. This page explains what trips it, what you'll see, and how to get back to normal. ## Why Bird slows or pauses sending Mailbox providers like Gmail and Yahoo judge senders by their track record. Mail that bounces, gets reported as spam, or drives a wave of unsubscribes damages that track record — and once a provider starts distrusting your domain, **everything** you send lands in spam or gets blocked, including the mail your recipients actually want. Reputation damage is much easier to prevent than to repair. So when Bird sees sustained harmful signals from your sending, it intervenes before mailbox providers do: first by **throttling** (slowing your sending rate), and if the signals are bad enough, by **pausing** sending entirely. The signals that drive this are the same ones you can watch yourself: - **Bounce rate** — the share of your sends going to addresses that don't exist. As guidance, a hard-bounce rate approaching **roughly 5%** is the ballpark where sending gets paused. Healthy lists run far below 1%, and mailbox providers start degrading your inbox placement well before 5%. - **Complaint rate** — the share of delivered mail recipients report as spam. This needs to stay **under 0.3%** — that's the hard ceiling Gmail and Yahoo enforce for bulk senders, and sustained rates above about 0.1% already hurt placement. - **Unsubscribe spikes** — unsubscribes are normal, but a sudden surge on a campaign is an early warning that you're mailing people who didn't expect it. Recipients who can't easily unsubscribe report spam instead. These thresholds are guidance, not hard cutoffs — enforcement weighs your volume, history, and trend, so two bounces in a ten-message test are treated very differently from the same rate across a million-recipient campaign. The developer-facing detail is on the [abuse & compliance page](/docs/guides/email/abuse-and-compliance). ## What you'll see - **Throttled**: your sends are accepted but go out more slowly than usual. Nothing fails — delivery just takes longer while your rates recover. - **Paused**: new sends are refused with a clear error explaining that sending is paused. - In both cases, a **notice in the dashboard** tells you what state your sending is in and which signal triggered it. A pause stops the damage from getting worse — every additional send to a bad list digs the hole deeper, so the refusal is protecting the recovery you're about to do. ## The path back to full speed The throttle responds to your signals, so recovery means fixing the source of the signals — not just waiting it out. Work through these in order: 1. **Stop sending to the addresses that caused it.** Hard-bounced and complaining addresses are already on your suppression list automatically, but if the campaign that triggered the pause is still queued or scheduled elsewhere, stop it. 2. **Clean your list.** Remove addresses that have bounced, haven't engaged in months, or were never properly opted in. If the list was purchased, scraped, or imported from an old system, this is almost certainly the root cause — the [list hygiene guide](/docs/knowledge-base/deliverability/list-hygiene) walks through the cleanup. 3. **Check your authentication.** Make sure your sending domain is verified and its DKIM and DMARC records are in place — unauthenticated mail is far more likely to be filtered and reported. 4. **Let your rates recover, then ramp gradually.** Once you resume, don't jump straight back to full volume. Start with your most engaged recipients and increase volume steadily, the same way you'd warm up a new domain — the [warming guide](/docs/knowledge-base/deliverability/warming) covers the ramp. Resuming with the same list that caused the pause will reproduce the same signals and the same pause — fix the list first, always. ## Staying out of trouble The best version of this page is the one you never need again. Watch your own bounce and complaint rates per campaign — they're visible in your sending metrics, and they're exactly what Bird's protective limits watch. The [reputation monitoring guide](/docs/knowledge-base/deliverability/reputation-monitoring) shows what to track and what healthy numbers look like. ## Next steps - [Reputation monitoring](/docs/knowledge-base/deliverability/reputation-monitoring) — the rates to watch and what healthy looks like - [List hygiene](/docs/knowledge-base/deliverability/list-hygiene) — the cleanup workflow that fixes most pauses - [Warming](/docs/knowledge-base/deliverability/warming) — ramping volume back up safely after a pause - [Abuse & compliance · Email](/docs/guides/email/abuse-and-compliance) — the developer-facing detail on signals and enforcement --- title: "SDKs & CLI" description: "The official ways to call Bird from your code: typed SDKs for TypeScript, Python, and Go over the curated surface, plus the bird CLI for shells and agents." source: https://bird.com/en-us/docs/sdks --- # SDKs & CLI You can call the Bird API with any HTTP client, but the official SDKs and the CLI handle authentication, region selection, retries, idempotency, and pagination for you — so you write against typed methods instead of raw requests. ## SDKs The SDKs are generated from the same OpenAPI specification that drives the [API reference](/docs/api), then wrapped in a hand-curated, idiomatic surface for each language. They share one behavior model — described once in [SDK concepts](/docs/sdks/concepts) — so retries, pagination, region inference, and webhook verification work the same everywhere. - [TypeScript SDK](/docs/sdks/typescript) — `@messagebird/sdk` for Node.js and the edge - [Python SDK](/docs/sdks/python) — sync and async clients - [Go SDK](/docs/sdks/go) — `github.com/messagebird/bird-sdk-go` - [SDK concepts](/docs/sdks/concepts) — the behavior every SDK shares ## CLI The [Bird CLI](/docs/cli) brings the same API to your terminal. It defaults to JSON output with semantic exit codes, which also makes it the simplest way to drive Bird from a script or an AI agent. --- title: "SDK concepts" description: "The behavior every Bird SDK shares — idempotency, retries, pagination, region inference, per-call options, and webhook verification." source: https://bird.com/en-us/docs/sdks/concepts --- # SDK concepts The [TypeScript](/docs/sdks/typescript), [Go](/docs/sdks/go), and [Python](/docs/sdks/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](/docs/guides/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). <!-- bird:snippet email.list.paginate --> ```typescript 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_cursor ``` <!-- bird:snippet email.list.paginate --> ```go 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 ``` <!-- bird:snippet email.list.paginate --> ```python 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) ``` The wire protocol — cursors, `limit`, `include_total` — is documented in the [pagination reference](/docs/api/pagination). ## 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](https://www.standardwebhooks.com/): 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](/docs/guides/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. ## Next steps - [Quickstarts](/docs/get-started/quickstarts) — send your first email in your language and framework. - [Webhooks & events](/docs/guides/webhooks) — endpoint setup and the event catalog behind `unwrap`. - [API reference](/docs/api) — the HTTP contract every SDK sits on. --- title: "Go SDK" description: "Install and configure github.com/messagebird/bird-sdk-go — client construction, typed errors, safe retries, pagination, and webhooks." source: https://bird.com/en-us/docs/sdks/go --- # 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. --- title: "Python SDK" description: "Install and configure the Bird Python SDK — sync and async clients, raised typed errors, safe retries, pagination, and webhooks." source: https://bird.com/en-us/docs/sdks/python --- # Python SDK `messagebird-sdk` (import name `bird`) is the official Python 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 [Python email quickstart](/docs/get-started/quickstarts/python/email). ## Install ```bash pip install messagebird-sdk ``` ```bash # or uv add messagebird-sdk poetry add messagebird-sdk ``` Requires Python 3.10+. The SDK is fully typed (`py.typed`), with Pydantic v2 response models. ## Create a client There are two clients: `Bird` (sync) and `AsyncBird` (async). They mirror each other method-for-method — `await` each call, `async for` over lists. Configuration is plain keyword arguments: <!-- bird:snippet email.send --> ```python msg = client.email.send( from_={"email": "onboarding@messagebird.dev", "name": "Bird"}, to=["delivered@messagebird.dev"], subject="Hello from Bird", html="<p>My first Bird email.</p>", ) print(msg.id, msg.status) ``` `from_` is the Python spelling of the wire field `from` (`from` is a reserved word); the alias is handled for you. Responses are Pydantic v2 models that tolerate unknown fields, so a new server field never breaks an existing client. `api_key` and `base_url` fall back to the `BIRD_API_KEY` / `BIRD_BASE_URL` environment variables, so `Bird()` with no arguments works when they are set. Use the client as a context manager (`with Bird() as client:` / `async with AsyncBird() as client:`) to close the underlying connection pool — and construct one client and reuse it; both are safe to share across threads or tasks. ### Configuration | Option | Description | | ------------------------ | ------------------------------------------------------------------------------ | | `api_key` | API key; falls back to `BIRD_API_KEY`. | | `region` / `base_url` | Region (or explicit base URL); falls back to the key prefix / `BIRD_BASE_URL`. | | `timeout`, `max_retries` | Request timeout and retry budget; overridable per call. | | `webhook_secret` | Signing secret for `client.webhooks.unwrap`. | | `email_defaults` | Client-wide `send` defaults; a per-send value always wins. | | `http_client` | Inject your own `httpx.Client` / `httpx.AsyncClient`. | Every method also takes a trailing `options` for per-call `timeout` / `max_retries` / `idempotency_key` / `extra_headers`, and `client.with_options(...)` derives a new client that reuses the parent's connection pool: <!-- bird:snippet email.options --> ```python client.email.send( from_={"email": "onboarding@messagebird.dev", "name": "Bird"}, to=["delivered@messagebird.dev"], subject="Hello from Bird", text="My first Bird email.", options={"timeout": 10, "max_retries": 0}, ) ``` ## How it's built The wire models are generated from Bird's OpenAPI spec; everything you touch is a hand-written, idiomatic layer on top — the curated resource surface (`client.email`, `client.webhooks`), explicit keyword arguments, and the request lifecycle (retries, timeouts, idempotency) in one core shared by every method. The cross-SDK model is in [SDK concepts](/docs/sdks/concepts). ## Errors Failures raise typed exceptions rooted at `BirdError`. `APIError` covers anything that goes wrong issuing a request — **including transport failures like timeouts** — so a single `except APIError` is enough for "the call failed". `APIStatusError` is the server-returned subset, carrying `status_code`, `request_id`, `code` (the stable `E#####` code), and `type` (the coarse [error category](/docs/guides/errors)). Below it sit `RateLimitError` (a 429, with `retry_after` in seconds) and `ValidationError` (a 422, with per-field `details`): <!-- bird:snippet email.errors --> ```python from bird import APIStatusError, RateLimitError, ValidationError try: client.email.send( from_={"email": "onboarding@messagebird.dev", "name": "Bird"}, to=["delivered@messagebird.dev"], subject="Hello from Bird", text="My first Bird email.", ) except RateLimitError as err: print("rate limited; retry after", err.retry_after) except ValidationError as err: print(err.status_code, err.details) except APIStatusError as err: print(err.status_code, err.code, err.request_id) ``` Transport-only failures are `APIConnectionError` and `APITimeoutError` — both subclasses of `APIError`, so they never slip past a broad `except APIError`. A bad webhook signature raises `WebhookVerificationError`. ## Safe retries Transient failures — timeouts, 429s, 5xx — retry automatically with jittered backoff that honors `Retry-After` (tune the budget with `max_retries`; zero disables). A mutation generates one idempotency key per logical call and reuses it across every retry attempt, so a retried send never double-applies. Pass `idempotency_key` in the per-call `options` to pin your own. ## Pagination List methods return a lazy page (`SyncPage` / `AsyncPage`); iterating it auto-paginates across cursors, fetching pages on demand: <!-- bird:snippet email.list.iterate --> ```python for message in client.email.list(status="delivered"): print(message.id) ``` <!-- bird:snippet email.list.async --> ```python from bird import AsyncBird async with AsyncBird() as client: async for message in client.email.list(status="delivered"): print(message.id) ``` Stop iterating and no further pages are fetched. ## Webhooks `client.webhooks.unwrap` verifies a [Standard Webhooks](https://www.standardwebhooks.com/) signature over the raw request body and returns a typed, discriminated event. Configure the signing secret on the client (`webhook_secret=`), and pass the exact bytes you received — parsing and re-serializing first breaks the signature: <!-- bird:snippet webhook.unwrap --> ```python # Pass the RAW request body (bytes) and the request headers. event = client.webhooks.unwrap(request.body, request.headers) if event.root.type == "email.delivered": print(event.root.data.email_id) ``` Verification is pure crypto — no network call — so it works the same in any web framework. ## 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.verbs --> ```python from bird import EmailMessage message = client.get("/v1/email/messages/em_01krd...", cast_to=EmailMessage) client.post("/v1/some/new/endpoint", body={"key": "value"}) ``` Find the paths in the [API reference](/docs/api). ## Next steps - [Python email quickstart](/docs/get-started/quickstarts/python/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. --- title: "TypeScript SDK" description: "Install and configure @messagebird/sdk, send your first request, and learn the client's retry, idempotency, and error model." source: https://bird.com/en-us/docs/sdks/typescript --- # TypeScript SDK `@messagebird/sdk` is the official TypeScript SDK for the Bird API — fully typed, ESM-only, and edge-ready. It runs on Node.js 20.3+ and modern edge runtimes (Cloudflare Workers, Vercel Edge, Deno) using only web-standard APIs (`fetch`, `AbortSignal`, Web Crypto). This page covers the client itself; for sending email with the SDK start with the [TypeScript email quickstart](/docs/get-started/quickstarts/typescript/email). ## Install ```bash npm install @messagebird/sdk # pnpm add @messagebird/sdk # yarn add @messagebird/sdk # bun add @messagebird/sdk ``` ## Construct a client <!-- bird:snippet client.options --> ```typescript const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY!, region: "eu1", // optional — override the region from the key prefix baseUrl: "http://localhost:8080", // optional — overrides region entirely (local/self-hosted) timeout: 60_000, // per-attempt timeout in ms (default 60_000) maxRetries: 2, // retry budget for transient failures (default 2) }); ``` Only `apiKey` is required. The region is inferred from the key's `bk_{region}_` prefix (a `bk_eu1_…` key routes to `https://eu1.platform.bird.com`), so most clients are constructed with the key alone — see [region inference](/docs/sdks/concepts#region-inference) for the resolution rules. You can also set channel defaults at construction (for example `email: { from: "hello@acme.com" }` makes `from` optional on every send) and the webhook signing secret via `webhooks: { secret }`. ## First call <!-- bird:snippet email.send --> ```typescript const msg = await bird.email.send({ from: { email: "onboarding@messagebird.dev", name: "Bird" }, to: ["delivered@messagebird.dev"], subject: "Hello from Bird", html: "<p>My first Bird email.</p>", }); console.log(msg.id, msg.status); // "em_…", "accepted" ``` `await` resolves to the API result directly — here an email message with its `em_*` id. For a runnable end-to-end walkthrough, follow the [TypeScript quickstart](/docs/get-started/quickstarts/typescript/email). ## Two-layer design The SDK is a generated layer plus a hand-owned one. Wire types and the low-level HTTP plumbing are generated from Bird's OpenAPI spec, so request and response shapes are always contract-accurate; the surface you actually call — `bird.email.send(...)`, the retry loop, idempotency, pagination, errors — is hand-written for ergonomics on top. Wire fields pass through verbatim in `snake_case` (`category`, `created_at`); only SDK-defined identifiers (method names, option keys like `idempotencyKey`) are `camelCase`. The same architecture is shared by the [Go](/docs/sdks/go) and [Python](/docs/sdks/python) SDKs — see [SDK concepts](/docs/sdks/concepts). ## Automatic idempotency and retries Every mutation (POST, PATCH, DELETE) gets an auto-generated `Idempotency-Key` header, and that one key is reused across every retry attempt — so a retried send can never deliver twice. Pass `{ idempotencyKey: "order-1234" }` in the per-call options to control the key yourself. Retries are on by default (`maxRetries: 2`). The client retries network failures, per-attempt timeouts, and transient statuses (408, 429, 500, 502, 503, 504) with jittered exponential backoff, honoring the server's `Retry-After` header when present. Deterministic failures (4xx like 401, 404, 422) are never retried. Set `maxRetries: 0` to disable, or override per call. The full lifecycle is described in [SDK concepts](/docs/sdks/concepts#safe-retries). ## Errors Methods throw on failure with a typed hierarchy you narrow with `instanceof`. `BirdError` is the root; `BirdAPIError` covers every error response from the server, with one subclass per error `type` — including `BirdAuthError` (401), `BirdRateLimitError` (429, with `retryAfter`), `BirdValidationError` (422, with per-field `details`), and `BirdPayloadTooLargeError` (413). Transport failures with no HTTP response are siblings of `BirdAPIError`: `BirdConnectionError` and `BirdTimeoutError`. <!-- bird:snippet email.errors --> ```typescript import { BirdRateLimitError, BirdValidationError, BirdAPIError } from "@messagebird/sdk"; try { await bird.email.send({ from: { email: "onboarding@messagebird.dev", name: "Bird" }, to: ["delivered@messagebird.dev"], subject: "Hello from Bird", html: "<p>My first Bird email.</p>", }); } catch (err) { if (err instanceof BirdRateLimitError) console.log(`rate limited — retry in ${err.retryAfter}s`); else if (err instanceof BirdValidationError) console.error(err.details); else if (err instanceof BirdAPIError) console.error(err.code, err.requestId); else throw err; } ``` Every `BirdAPIError` carries `statusCode`, `type`, `code` (the stable `E#####` error code), `requestId`, and `docUrl`. Branch on the class (or the coarse `type`) for control flow; use `code` when you need to match one specific failure. Prefer branching on a value instead of catching? Every call also has `.safe()`: <!-- bird:snippet email.safe --> ```typescript const { data, error } = await bird.email .send({ from: { email: "onboarding@messagebird.dev", name: "Bird" }, to: ["delivered@messagebird.dev"], subject: "Hello from Bird", html: "<p>My first Bird email.</p>", }) .safe(); if (error) console.error(error.message); else console.log(data.id); ``` ## Webhooks `bird.webhooks.unwrap(rawBody, headers)` verifies an inbound delivery's [Standard Webhooks](https://www.standardwebhooks.com/) signature and returns a typed, discriminated event. Always pass the raw request body — the signature is computed over the raw bytes, so parsing and re-serializing first is the classic webhook bug. A bad signature, stale timestamp, or malformed headers throws `BirdWebhookVerificationError`. See [webhook verification](/docs/sdks/concepts#webhook-verification) for the cross-SDK contract and [Webhooks](/docs/guides/webhooks) for the platform side. ## Next steps - [Email quickstart](/docs/get-started/quickstarts/typescript/email) — `send`, `get`, `list`, channel defaults, and the response shapes. - [SDK concepts](/docs/sdks/concepts) — idempotency, retries, pagination, regions, and webhooks across all Bird SDKs. - [API reference](/docs/api) — the underlying HTTP API; `bird.request<T>()` reaches any endpoint the typed surface doesn't cover yet, with the same auth, retries, and idempotency.