// security workers

Email and messages

[ view markdown ]

Score email, SMS, WhatsApp, and social messages before agent delivery.

Use this capability only for approved content that does not contain PII or PHI. Apply data classification and filtering before sending raw email or message text to Superagent.

Email

Score a raw RFC 822 / .eml message before an agent reads it, summarizes it, or follows links inside it.

How scoring works

An email scan runs in three tiers:

  1. Identity — From domain RDAP/TLS/blocklists, SPF/DKIM/DMARC from Authentication-Results, display-name mismatch, and Reply-To divergence. A blocklisted sender short-circuits to score 0.
  2. Body and attachments — prompt injection, exfiltration, urgency, brand impersonation, hidden HTML, encoded payloads, and dangerous attachment types. Up to eight public https links are followed with the web-page scanner.
  3. LLM review — always runs unless the sender is blocklisted. Looks for phishing, brand impersonation, and hidden agent instructions.

The raw message is not stored on the artifact. Cache lookup uses the SHA-256 of the raw bytes. Invalid identifiers or RFC 822 bodies return 400 invalid_request. Unknown email identifiers return 404 not_found.

Threats

When details=true or the verdict is suspicious / dangerous, email scans may emit spf_fail, display_name_mismatch, brand_impersonation, malicious_attachment, phishing, and prompt-injection or exfiltration types shared with other origins.

Use it

  • Dashboard: open Agents → Context, paste a raw message, and open the result for identity, behavior, and content scores.
  • REST API: POST /api/v1/context/email to score raw email and GET /api/v1/context/email/{identifier} to look up a previous result. See the Context Guardrails API.
  • Resend: scan inbound mail from a receiving domain. See Resend.
  • Bird: handle email.received, fetch the stored raw RFC 5322 message, and run the full email scan. See Bird.
  • MCP: scan_email.

Submit an email

Use Content-Type: message/rfc822 or text/plain. Raw bodies larger than 1 MB are rejected.

curl "https://superagent.sh/api/v1/context/email?details=true" \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: message/rfc822" \
  --data-binary @message.eml
curl "https://superagent.sh/api/v1/context/email/<sha256>?details=true" \
  -H "Authorization: Bearer sk_live_..."

To rescan an email, POST the raw body again. GET never starts a new scan.

Messages

Score a normalized SMS, WhatsApp, or social message before an agent reads it or follows a link inside it. The message endpoint scans text and outbound https links. It does not scan media or attachments.

Use the email endpoint instead for email. Email scans can inspect sender identity and, when you submit raw RFC 822, authentication headers, MIME structure, and attachments.

Request

Send application/json to POST /api/v1/context/message:

Field Type Required Description
text string yes Message text to scan
channel string no Provider-neutral channel label such as sms, whatsapp, or instagram
links string[] no Outbound https links associated with the message
curl "https://superagent.sh/api/v1/context/message?mode=full&details=true" \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  --data '{
    "text": "Review the account notice at https://login.example.net/reset",
    "channel": "whatsapp",
    "links": ["https://login.example.net/reset"]
  }'

The response uses the standard Context Guardrails API envelope. Use mode=full when you need the completed verdict in the same request, and use details=true to include all detected threats.

The artifact identifier is the SHA-256 digest of the canonical channel, text, and link fields. Look up a completed result without starting another scan:

curl "https://superagent.sh/api/v1/context/message/<sha256>?details=true" \
  -H "Authorization: Bearer sk_live_..."

What gets scanned

Message scans evaluate untrusted text for phishing, credential harvesting, social engineering, prompt injection, and exfiltration patterns. Outbound https links are checked as web content.

Identity is not scored for social messages. The identity sub-score is a neutral compatibility value with zero weight, and the identity tier is reported as skipped. Completed message confidence is capped at medium.

The endpoint is intentionally text only. Images, audio, video, documents, stickers, and other media are out of scope. Do not treat a safe message verdict as a verdict on omitted media or attachments.

Integrate a provider

Verify the provider's webhook signature against the raw request before parsing or normalizing the payload. Then map its text, channel name, and outbound https links into the provider-neutral request above.

For Bird Channels webhooks, see the Bird integration.

Resend

Resend is an email API for developers. Wire Context Guardrails into Resend's inbound webhook pipeline to scan approved inbound messages for phishing, credential harvesting, prompt injection, and social engineering before your application processes them.

This configuration excludes messages containing PII or PHI. Apply the application's data classification and filtering before fetching or sending raw email to Superagent. Do not log sender addresses, message bodies, or other personal data.

Prerequisites

  • a Superagent organization API key from Settings
  • a Resend account and API key
  • a receiving domain restricted to synthetic or approved messages without PII or PHI
  • a publicly reachable HTTPS endpoint to receive webhooks

How it works

flowchart TD
    A[Incoming email] --> B[Resend inbound]
    B --> C["email.received webhook"]
    C --> D["Fetch raw email via Receiving API"]
    D --> E["POST /api/v1/context/email"]
    E -->|safe or caution| F[Process email]
    E -->|suspicious or dangerous| G[Discard / alert]

Resend fires an email.received webhook when a message arrives at your receiving domain. The webhook payload contains metadata only (from, to, subject, email_id) — not the email body. Your handler fetches the raw email via Resend's Receiving API and POSTs it to Superagent's /context/email endpoint.

Sending the raw RFC 822 content preserves headers, MIME structure, and authentication signals (SPF, DKIM) for more accurate phishing and identity checks. If the verdict is suspicious or dangerous, the handler discards the email or triggers an alert. Treat caution as review-before-use.

Setup

1. Configure an email.received webhook

In the Resend dashboard, add a webhook subscribed to the email.received event pointing at your handler URL. Verify webhook signatures before you trust the payload. You can also create the webhook via the SDK:

import resend

resend.api_key = "re_..."

resend.Webhooks.create({
    "url": "https://your-server.com/webhooks/resend",
    "events": ["email.received"],
})
import { Resend } from "resend";

const resend = new Resend("re_...");

await resend.webhooks.create({
  url: "https://your-server.com/webhooks/resend",
  events: ["email.received"],
});

2. Build a webhook handler that scans with Superagent

When an email.received event arrives, fetch the raw email from the Receiving API and POST it to Superagent. Use mode=full so the handler waits for the completed score (up to 90 seconds) instead of a preliminary identity result. Set your HTTP client timeout accordingly.

If your platform's webhook timeout is shorter than that, POST without mode=full, return 200 to Resend, and poll GET /api/v1/context/email/{identifier} until pending_deep_scan is false.

import os

import httpx
import resend
from fastapi import FastAPI, Request

app = FastAPI()
resend.api_key = os.environ["RESEND_API_KEY"]

@app.post("/webhooks/resend")
async def handle_email_received(request: Request):
    # Bind this handler only to the restricted receiving domain described above.
    event = await request.json()
    if event.get("type") != "email.received":
        return {"status": "ignored"}

    email = resend.Emails.Receiving.get(email_id=event["data"]["email_id"])
    download_url = (email.get("raw") or {}).get("download_url")
    if not download_url:
        return {"status": "error", "message": "raw email not available"}

    async with httpx.AsyncClient(timeout=90.0) as http:
        eml = await http.get(download_url)
        eml.raise_for_status()

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

    artifact = scan.json()["data"]
    verdict = artifact.get("verdict", "safe")

    if verdict in ("suspicious", "dangerous"):
        print(
            f"Blocked email: verdict={verdict}, score={artifact.get('score')}"
        )
        return {"status": "blocked", "verdict": verdict}

    # Deliver the message to your agent or application.
    return {"status": "processed"}
import { Resend } from "resend";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";

const resend = new Resend(process.env.RESEND_API_KEY);

export const POST = async (request: NextRequest) => {
  // Bind this handler only to the restricted receiving domain described above.
  const event = await request.json();
  if (event.type !== "email.received") {
    return NextResponse.json({ status: "ignored" });
  }

  const { data: email } = await resend.emails.receiving.get(
    event.data.email_id,
  );
  const downloadUrl = email?.raw?.download_url;
  if (!downloadUrl) {
    return NextResponse.json(
      { status: "error", message: "raw email not available" },
      { status: 500 },
    );
  }

  const eml = await fetch(downloadUrl);
  const emlBody = await eml.text();

  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: emlBody,
      signal: AbortSignal.timeout(90_000),
    },
  );

  const payload = await scan.json();
  const artifact = payload.data;
  const verdict = artifact.verdict ?? "safe";

  if (verdict === "suspicious" || verdict === "dangerous") {
    console.log(
      `Blocked email: verdict=${verdict}, score=${artifact.score}`,
    );
    return NextResponse.json({ status: "blocked", verdict });
  }

  // Deliver the message to your agent or application.
  return NextResponse.json({ status: "processed" });
};

Return 2xx for both processed and blocked messages so Resend does not retry a scan you already completed.

3. Deploy and test

Deploy your webhook handler and send a test email to your Resend receiving address. Check the logs to confirm Superagent is scanning each message and returning a verdict.

What gets detected

Context Guardrails checks sender identity, the body and attachments, and outbound links. See email scoring for the full scan model.

Threat Description
phishing Credential harvesting, urgency lures, or dangerous followed links
social_engineering Deceptive content designed to manipulate a person or agent
prompt_injection Hidden instructions aimed at an agent that reads the message
exfiltration Patterns designed to leak session data or secrets
brand_impersonation Message impersonates a known brand from an unrelated sender domain
malicious_attachment Dangerous attachment types
spf_fail / display_name_mismatch Sender authentication or display-name problems

Each threat is assigned a severity (critical, high, medium, low). Pass details=true to include threats even when the verdict is safe.

Using the response

The Superagent response follows the standard API envelope:

{
  "data": {
    "object": "context_artifact",
    "id": "00000000-0000-4000-8000-000000000001",
    "origin": "email",
    "identifier": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "url": "email:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "score": 15,
    "verdict": "dangerous",
    "confidence": "high",
    "tolerance": "conservative",
    "scanned_at": "2026-09-01T12:00:00.000Z",
    "pending_deep_scan": false,
    "threats": [
      {
        "type": "phishing",
        "severity": "critical",
        "detail": "Credential harvesting form targeting Google login"
      }
    ]
  }
}

Verdicts are safe, caution, suspicious, and dangerous. You can also read them from response headers (x-superagent-verdict, x-superagent-score, x-superagent-confidence, x-superagent-tolerance) for lightweight checks without parsing JSON.

Bird

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.

Agent prompt

Copy this prompt into your coding agent to wire up the Bird flow:

Integrate Superagent Context Guardrails into this app's Bird inbound messaging pipeline.

Read https://www.superagent.sh/docs/security-workers/agent-guardrails/email-and-messages and follow it.

Do not send email or messages containing PII or PHI to Superagent. Apply the application's data classification and filtering before this integration.

1. Subscribe one Bird Standard Webhooks endpoint to email.received, sms.received, and whatsapp.received.
2. Verify webhook-id, webhook-timestamp, and webhook-signature against the raw request body with BIRD_WEBHOOK_SECRET.
3. Deduplicate on webhook-id, persist the verified event, enqueue processing, and return 2xx within five seconds.
4. For email.received, fetch GET /v1/email/inbound-messages/{inbound_message_id}/raw and POST the RFC 5322 bytes to https://superagent.sh/api/v1/context/email?mode=full&details=true with Content-Type: message/rfc822.
5. For sms.received and whatsapp.received, extract the text content and POST it to https://superagent.sh/api/v1/context/message?mode=full&details=true with the channel name.
6. Do not claim media or attachments were scanned by the message endpoint.

Block or alert on suspicious or dangerous. Treat caution as review before use.

Use SUPERAGENT_API_KEY, BIRD_API_KEY, BIRD_API_BASE_URL, and BIRD_WEBHOOK_SECRET from the environment. Do not hardcode secrets.

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