Integration Guide
Vercel AI SDK
Add postal 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.).
New mailbox.bot-issued physical receiving addresses and associated package receiving: Not live yet · Launch ETA: mid/late September. Reservations are open, but a reservation does not assign or activate an address. Assigned-PMB examples below apply only to accounts that already have receiving enabled.
Install
npm install ai @ai-sdk/openai zodTool Definitions
Create 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 listPhysicalInbound = tool({
description: "List physical letters, flats, and parcels at the assigned PMB",
inputSchema: z.object({}),
execute: async () => {
const res = await fetch(`${BASE}/inbound-items`, { headers });
return res.ok ? JSON.stringify(await res.json()) : `Error: ${await res.text()}`;
},
});
export const requestOpenAndScan = tool({
description: "Request the only supported physical action after member approval",
inputSchema: z.object({
inboundItemId: z.string(),
expectedVersion: z.number().int().nonnegative(),
}),
execute: async ({ inboundItemId, expectedVersion }) => {
const res = await fetch(`${BASE}/inbound-items/${inboundItemId}/actions`, {
method: "POST",
headers: {
...headers,
"Content-Type": "application/json",
"Idempotency-Key": `open-scan-${inboundItemId}-${expectedVersion}`,
},
body: JSON.stringify({
action_type: "open_and_scan",
expected_version: expectedVersion,
}),
});
return res.ok ? JSON.stringify(await res.json()) : `Error: ${await res.text()}`;
},
});
export const getPhysicalScanResults = tool({
description: "Get completed scans for one physical inbound item",
inputSchema: z.object({
inboundItemId: z.string(),
}),
execute: async ({ inboundItemId }) => {
const res = await fetch(`${BASE}/inbound-items/${inboundItemId}/scans`, { headers });
return res.ok ? JSON.stringify(await res.json()) : `Error: ${await res.text()}`;
},
});
export const sendLetter = tool({
description: "Print and mail a physical letter. Provide the PDF as base64.",
inputSchema: 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}`;
},
});
Server-Side: generateText / streamText
// app/api/mail-agent/route.ts
import { streamText, stepCountIs, UIMessage, convertToModelMessages } from "ai";
import { openai } from "@ai-sdk/openai";
import {
listPhysicalInbound, requestOpenAndScan,
getPhysicalScanResults, sendLetter,
} from "@/lib/mailbox-tools";
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: openai("gpt-4o"),
system: `Keep forwarded digital/OCR context separate from physical custody.
For assigned-PMB items, request only open_and_scan after member approval,
with the current item version. Infer no other physical handling action.`,
messages: await convertToModelMessages(messages),
tools: { listPhysicalInbound, requestOpenAndScan,
getPhysicalScanResults, sendLetter },
stopWhen: stepCountIs(5), // allow multi-step tool use
});
return result.toUIMessageStreamResponse();
}Client-Side: useChat Hook
// app/mail/page.tsx
"use client";
import { useState } from "react";
import { useChat } from "@ai-sdk/react";
export default function MailAssistant() {
const [input, setInput] = useState("");
const { messages, sendMessage } = useChat({
api: "/api/mail-agent",
});
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong>
{m.parts.map((part, i) =>
part.type === "text" ? <span key={i}>{part.text}</span> : null
)}
</div>
))}
<form onSubmit={(e) => {
e.preventDefault();
sendMessage({ text: input });
setInput("");
}}>
<input value={input} onChange={(e) => setInput(e.target.value)}
placeholder="Check the mailbox..." />
</form>
</div>
);
}Webhook Route Handler
For an account that already has an assigned PMB and receiving enabled, 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";
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 || v1.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))) {
return new Response("Invalid signature", { status: 401 });
}
const event = JSON.parse(body);
if (event.event_type === "inbound.received") {
console.info("Physical inbound received", {
inboundItemId: event.inbound_item_id,
instruction: "Report arrival; request only Open & Scan after member approval.",
});
}
return new Response("ok");
}