Documentation
Sign inGet started

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 or the bird CLI.

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:
Contoh kode
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:
Contoh kode
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: 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:
Contoh kode
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 carry the identity fields, not your tags or metadata, so fetch the message by email_id when you need your own context back, 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'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.
Contoh kode
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:
Contoh kode
{
  "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:
Contoh kode
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. 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 support makes the retry safe — generate one Idempotency-Key per logical operation (not per attempt) and reuse it on every retry:
Contoh kode
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:
Contoh kode
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.

Go deeper

  • MCP server — the tool surface these patterns drive, hosted at mcp.platform.bird.com or run locally with the CLI
  • CLI for agents — the same operations for shell-capable agents
  • Webhooks & events — delivery semantics, signatures, and the event catalog behind Pattern 2
  • Idempotency — replay semantics and failure modes behind Pattern 5
  • Errors — the envelope and the full error-code catalog
  • Mail sandbox — the magic-address matrix behind Pattern 3