// integrations

Bird

Scan inbound Bird email, SMS, WhatsApp, and social messages with Context Guardrails before your application processes them.

Bird provides email, SMS, WhatsApp, and social messaging APIs. Put Context Guardrails between Bird and your agent so untrusted content is scored before the agent reads it or follows links.

This guide uses Bird's current Standard Webhooks contract consistently for
email.received, sms.received, and whatsapp.received.

Prerequisites

  • a Superagent organization API key from Settings
  • a Bird workspace with inbound email or messaging channels configured
  • a Bird regional API key
  • a publicly reachable HTTPS webhook endpoint
  • a stored Standard Webhooks signing secret

How it works

flowchart TD
    A[Inbound Bird content] --> C["Standard Webhook"]
    C --> D[Verify signature]
    D --> E["Fetch /v1/email/inbound-messages/{id}/raw"]
    D --> I["Extract SMS or WhatsApp text"]
    E --> F["POST /api/v1/context/email"]
    I --> messageApi["POST /api/v1/context/message"]
    F --> K{Verdict}
    messageApi --> K
    K -->|safe| L[Deliver to agent]
    K -->|caution| M[Review]
    K -->|suspicious or dangerous| N[Quarantine or alert]

Standard Webhooks setup

1. Subscribe to inbound events

Create a Standard Webhooks endpoint in Developers → Webhooks, with the Bird CLI, or through POST /v1/webhooks. Use the regional host and matching key for your workspace, such as https://us1.platform.bird.com or https://eu1.platform.bird.com.

curl --request POST "$BIRD_API_BASE_URL/v1/webhooks" \
  --header "Authorization: Bearer $BIRD_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "url": "https://your-server.com/webhooks/bird",
    "events": ["email.received", "sms.received", "whatsapp.received"],
    "description": "Scan inbound messages before agent delivery"
  }'

The response returns a whsec_... secret once. Store it as BIRD_WEBHOOK_SECRET.

2. Verify and queue the webhook

Bird Standard Webhooks sends webhook-id, webhook-timestamp, and webhook-signature. Verify the signature over the untouched request body and reject timestamps more than five minutes old. Bird requires a response within five seconds, so durably enqueue the verified event before returning 2xx.

import { createHmac, timingSafeEqual } from "node:crypto";

function verifyBirdWebhook(
  rawBody: string,
  headers: Headers,
  secret: string,
) {
  const id = headers.get("webhook-id") ?? "";
  const timestamp = headers.get("webhook-timestamp") ?? "";
  const signatures = headers.get("webhook-signature") ?? "";
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = createHmac("sha256", key)
    .update(`${id}.${timestamp}.${rawBody}`)
    .digest();

  return signatures.split(" ").some((part) => {
    const received = Buffer.from(part.replace(/^v1,/, ""), "base64");
    return (
      received.length === expected.length &&
      timingSafeEqual(received, expected)
    );
  });
}

export async function POST(request: Request) {
  const rawBody = await request.text();
  if (
    !verifyBirdWebhook(
      rawBody,
      request.headers,
      process.env.BIRD_WEBHOOK_SECRET ?? "",
    )
  ) {
    return Response.json({ error: "invalid signature" }, { status: 401 });
  }

  const event = JSON.parse(rawBody);
  const supported = new Set([
    "email.received",
    "sms.received",
    "whatsapp.received",
  ]);
  if (!supported.has(event.type)) {
    return Response.json({ status: "ignored" });
  }

  // Atomically deduplicate on webhook-id and persist before acknowledging.
  await enqueueInboundEvent({
    webhookId: request.headers.get("webhook-id"),
    event,
  });
  return Response.json({ status: "queued" }, { status: 202 });
}

The handler only depends on the stable envelope fields needed for scanning:

{
  "type": "email.received",
  "timestamp": "2026-09-03T12:00:00Z",
  "data": {
    "inbound_message_id": "inm_01k4example",
    "subject": "Account review",
    "authentication": "pass",
    "spf": "pass",
    "dkim": "pass",
    "dmarc": "pass"
  }
}

Inbound email

Bird's receiving email documentation defines the email-specific processing flow:

  1. Bird emits email.received after storing and parsing the message.
  2. data.inbound_message_id identifies the stored inbound email.
  3. GET /v1/email/inbound-messages/{id}/raw returns the original RFC 5322 MIME message.

Fetching the raw message preserves sender headers, SPF/DKIM/DMARC results, MIME structure, attachment names, HTML, text, and links for the full Superagent email scan.

Fetch the raw email and scan it

Run this in the queued worker:

async function processInboundEmail(inboundMessageId: string) {
  const rawEmail = await fetch(
    `${process.env.BIRD_API_BASE_URL}/v1/email/inbound-messages/${encodeURIComponent(inboundMessageId)}/raw`,
    {
      headers: {
        Authorization: `Bearer ${process.env.BIRD_API_KEY}`,
      },
    },
  );
  if (!rawEmail.ok) {
    throw new Error(`Bird raw email fetch failed: ${rawEmail.status}`);
  }

  const scan = await fetch(
    "https://superagent.sh/api/v1/context/email?mode=full&details=true",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.SUPERAGENT_API_KEY}`,
        "Content-Type": "message/rfc822",
      },
      body: await rawEmail.arrayBuffer(),
      signal: AbortSignal.timeout(90_000),
    },
  );
  if (!scan.ok) {
    throw new Error(`Superagent email scan failed: ${scan.status}`);
  }

  const artifact = (await scan.json()).data;
  if (
    artifact.verdict === "suspicious" ||
    artifact.verdict === "dangerous"
  ) {
    return { status: "blocked", artifact };
  }
  if (artifact.verdict === "caution") {
    return { status: "review", artifact };
  }

  // Deliver the raw email or approved extracted content to the agent.
  return { status: "processed", artifact };
}

Python uses the same sequence:

import os

import httpx


async def process_inbound_email(inbound_message_id: str):
    async with httpx.AsyncClient(timeout=90.0) as client:
        raw = await client.get(
            (
                f"{os.environ['BIRD_API_BASE_URL']}/v1/email/"
                f"inbound-messages/{inbound_message_id}/raw"
            ),
            headers={"Authorization": f"Bearer {os.environ['BIRD_API_KEY']}"},
        )
        raw.raise_for_status()

        scan = await client.post(
            (
                "https://superagent.sh/api/v1/context/email"
                "?mode=full&details=true"
            ),
            content=raw.content,
            headers={
                "Authorization": (
                    f"Bearer {os.environ['SUPERAGENT_API_KEY']}"
                ),
                "Content-Type": "message/rfc822",
            },
        )
        scan.raise_for_status()

    artifact = scan.json()["data"]
    if artifact["verdict"] in ("suspicious", "dangerous"):
        return {"status": "blocked", "artifact": artifact}
    if artifact["verdict"] == "caution":
        return {"status": "review", "artifact": artifact}
    return {"status": "processed", "artifact": artifact}

SMS and WhatsApp

Bird's current SMS events and WhatsApp events use the same Standard Webhooks envelope and verifier as email.

sms.received carries the inbound body, both phone numbers, and segment metadata. whatsapp.received carries one content arm such as text, interactive_reply, image, document, or another supported WhatsApp type. Send only textual content to the message endpoint:

function receivedText(event: {
  type: string;
  data?: Record<string, any>;
}) {
  if (event.type === "sms.received") {
    const body = event.data?.body;
    return typeof body === "string" ? body : body?.text;
  }
  if (event.type === "whatsapp.received") {
    const text = event.data?.text;
    return typeof text === "string" ? text : text?.text;
  }
  return undefined;
}

async function processInboundMessage(event: {
  type: "sms.received" | "whatsapp.received";
  data: Record<string, any>;
}) {
  const text = receivedText(event);
if (!text) {
    return {
    status: "ignored",
    reason: "media and attachments are not scanned",
    };
}

const scan = await fetch(
  "https://superagent.sh/api/v1/context/message?mode=full&details=true",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SUPERAGENT_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      text,
        channel: event.type.replace(/\.received$/, ""),
    }),
  },
);
  if (!scan.ok) {
    throw new Error(`Superagent message scan failed: ${scan.status}`);
  }

  const artifact = (await scan.json()).data;
  if (
    artifact.verdict === "suspicious" ||
    artifact.verdict === "dangerous"
  ) {
    return { status: "blocked", artifact };
  }
  if (artifact.verdict === "caution") {
    return { status: "review", artifact };
  }
  return { status: "processed", artifact };
}

The message verdict covers text and outbound HTTPS links only. It does not cover images, audio, video, documents, stickers, or attachments.

For another social platform, first confirm its current inbound event in the workspace event catalog and subscribe to that exact event type. Keep the same Standard Webhooks verification and normalization flow; do not fall back to the legacy signature contract in this integration.

Test it

  1. Use Bird's webhook test send to verify connectivity and signature handling. Test sends contain only a minimal event stub.
  2. Send a real message to a Bird forwarding address or receiving domain for an end-to-end email test.
  3. Confirm the worker fetches /raw, Superagent reports origin: "email", and the email artifact includes authentication, MIME, attachment, and link evidence.
  4. Send a test SMS or social message and confirm it produces origin: "message".

Verdicts are safe, caution, suspicious, and dangerous. Return 2xx only after the webhook event is durably queued. Bird retries failed Standard Webhook deliveries up to eight times.

Next steps