Integration Guide
Add physical mail tools to any Vercel AI SDK project. Works with streamText, generateText, and the useChat hook. Compatible with any model provider (OpenAI, Anthropic, Google, etc.).
npm install ai @ai-sdk/openai zodCreate a lib/mailbox-tools.ts file with your mail tools. Each tool uses zod for parameter validation.
// lib/mailbox-tools.ts
import { tool } from "ai";
import { z } from "zod";
const API_KEY = process.env.MAILBOX_BOT_API_KEY!;
const BASE = "https://mailbox.bot/api/v1";
const headers = { Authorization: `Bearer ${API_KEY}` };
const AGENT_ID = process.env.MAILBOX_BOT_AGENT_ID!;
// Fetch MAILBOX.md version (required for outbound mail)
async function getMdVersion(): Promise<string> {
const res = await fetch(`${BASE}/agents/${AGENT_ID}/instructions`, { headers });
const data = await res.json();
return String(data.version);
}
export const checkMailbox = tool({
description: "Check for new physical mail at your postal mailing address",
parameters: z.object({}),
execute: async () => {
const res = await fetch(`${BASE}/packages?status=received`, { headers });
const { packages } = await res.json();
if (!packages?.length) return "No new mail.";
return packages
.map((p: any) =>
`- ${p.carrier || "USPS"} | ${p.status} | photos: ${p.photos_count || 0} (id: ${p.id})`
)
.join("\n");
},
});
export const scanMail = tool({
description: "Request a document scan with OCR for a piece of mail",
parameters: z.object({
packageId: z.string().describe("Package ID from checkMailbox results"),
}),
execute: async ({ packageId }) => {
const res = await fetch(`${BASE}/packages/${packageId}/actions`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ action_type: "open_and_scan" }),
});
if (!res.ok) return `Error: ${await res.text()}`;
const data = await res.json();
return `Scan requested. Action ID: ${data.action_request.id}`;
},
});
export const getScanResults = tool({
description: "Get OCR scan results for a previously scanned piece of mail",
parameters: z.object({
packageId: z.string().describe("Package ID to get scan results for"),
}),
execute: async ({ packageId }) => {
const res = await fetch(`${BASE}/packages/${packageId}/scan`, { headers });
const { scans } = await res.json();
if (!scans?.length) return "No scan results yet.";
return `OCR text:\n${scans[0].ocr_text || "N/A"}`;
},
});
export const forwardMail = tool({
description: "Forward a piece of mail to another US address",
parameters: z.object({
packageId: z.string(),
name: z.string().describe("Recipient name"),
address: z.string().describe("Street address"),
city: z.string(),
state: z.string().length(2),
zip: z.string(),
}),
execute: async ({ packageId, name, address, city, state, zip }) => {
const res = await fetch(`${BASE}/packages/${packageId}/actions`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
action_type: "forward",
parameters: {
forward_name: name,
forward_address: address,
forward_city: city,
forward_state: state,
forward_zip: zip,
},
}),
});
if (!res.ok) return `Error: ${await res.text()}`;
return `Forward requested. Action ID: ${(await res.json()).action_request.id}`;
},
});
export const sendLetter = tool({
description: "Print and mail a physical letter. Provide the PDF as base64.",
parameters: z.object({
recipientName: z.string(),
recipientLine1: z.string(),
recipientCity: z.string(),
recipientState: z.string().length(2),
recipientZip: z.string(),
pdfBase64: z.string().describe("Base64-encoded PDF file content"),
mailClass: z.enum([
"first_class", "priority", "certified",
"certified_return_receipt",
]).default("first_class"),
}),
execute: async (params) => {
const form = new FormData();
const pdf = Buffer.from(params.pdfBase64, "base64");
form.append("document", new Blob([pdf], { type: "application/pdf" }), "letter.pdf");
form.append("recipient_name", params.recipientName);
form.append("recipient_line1", params.recipientLine1);
form.append("recipient_city", params.recipientCity);
form.append("recipient_state", params.recipientState);
form.append("recipient_zip", params.recipientZip);
form.append("mail_class", params.mailClass);
const res = await fetch(`${BASE}/mail`, {
method: "POST",
headers: { ...headers, "X-Mailbox-MD-Version": await getMdVersion() },
body: form,
});
if (!res.ok) return `Error: ${await res.text()}`;
return `Letter queued. ID: ${(await res.json()).outbound_mail.id}`;
},
});
export const shredMail = tool({
description: "Shred and securely dispose of a piece of junk mail",
parameters: z.object({
packageId: z.string().describe("Package ID to shred"),
}),
execute: async ({ packageId }) => {
const res = await fetch(`${BASE}/packages/${packageId}/actions`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ action_type: "shred" }),
});
if (!res.ok) return `Error: ${await res.text()}`;
return `Shred requested. Action ID: ${(await res.json()).action_request.id}`;
},
});// app/api/mail-agent/route.ts
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
import {
checkMailbox, scanMail, getScanResults,
forwardMail, sendLetter, shredMail,
} from "@/lib/mailbox-tools";
export async function POST(req: Request) {
const { prompt } = await req.json();
const result = streamText({
model: openai("gpt-4o"),
system: `You manage physical mail for the organization.
Scan anything from government agencies or the IRS immediately.
Shred marketing mail and credit card offers.
Report important mail with sender and content summary.`,
prompt,
tools: { checkMailbox, scanMail, getScanResults,
forwardMail, sendLetter, shredMail },
maxSteps: 5, // allow multi-step tool use
});
return result.toDataStreamResponse();
}// app/mail/page.tsx
"use client";
import { useChat } from "@ai-sdk/react";
export default function MailAssistant() {
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: "/api/mail-agent",
});
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange}
placeholder="Check the mailbox..." />
</form>
</div>
);
}Process inbound mail events as they arrive. Add this Next.js route handler to trigger your agent automatically.
// app/api/webhooks/mailbox-bot/route.ts
import crypto from "crypto";
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { scanMail, shredMail } from "@/lib/mailbox-tools";
const WEBHOOK_SECRET = process.env.MAILBOX_BOT_WEBHOOK_SECRET!;
export async function POST(req: Request) {
const body = await req.text();
const sigHeader = req.headers.get("x-mailbox-signature") || "";
// Signature format: whsk_prefix:t=<timestamp>,v1=<hmac-hex>
const colonIdx = sigHeader.indexOf(":");
const sigData = sigHeader.slice(colonIdx + 1);
const params = Object.fromEntries(
sigData.split(",").map((p) => p.split("=", 2))
);
const ts = params.t || "";
const v1 = params.v1 || "";
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(`${ts}.${body}`)
.digest("hex");
if (!v1 || !crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))) {
return new Response("Invalid signature", { status: 401 });
}
const event = JSON.parse(body);
if (event.event === "package.received") {
await generateText({
model: openai("gpt-4o"),
tools: { scanMail, shredMail },
prompt: `New mail received. Carrier: ${event.carrier || "USPS"}.
Package ID: ${event.package_id}.
If government or legal, scan it. If marketing or junk, shred it.`,
maxSteps: 3,
});
}
return new Response("ok");
}