Integration Guide
OpenAI Agents SDK
Add mailbox.bot to agents built with the OpenAI Agents SDK. Keep forwarded digital/OCR inbound context separate from account-enabled physical custody, request only Open & Scan, and send outbound mail with approval and dry-run support.
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
bash
pip install openai-agents requestsAuthentication
python
import os
API_KEY = os.environ["MAILBOX_BOT_API_KEY"] # sk_agent_*
BASE = "https://mailbox.bot/api/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
AGENT_ID = os.environ["MAILBOX_BOT_AGENT_ID"] # from dashboardFunction Tools
python
import json
from typing import Literal
from agents import Agent, Runner, function_tool
import requests
session = requests.Session()
session.headers.update(HEADERS)
MailClass = Literal[
"first_class",
"priority",
"certified",
"certified_return_receipt",
"fedex_ground",
"fedex_express",
"fedex_2day",
"fedex_overnight",
"ups_ground",
"ups_2day",
"ups_next_day",
]
def api_get(path: str, **kwargs):
r = session.get(f"{BASE}{path}", timeout=30, **kwargs)
r.raise_for_status()
return r
def api_post(path: str, *, headers: dict | None = None, **kwargs):
merged_headers = dict(HEADERS)
if headers:
merged_headers.update(headers)
r = session.post(f"{BASE}{path}", headers=merged_headers, timeout=60, **kwargs)
r.raise_for_status()
return r
def get_mailbox_md_version() -> str:
r = api_get(f"/agents/{AGENT_ID}/instructions")
return str(r.json()["version"])
def mailbox_md_headers() -> dict:
return {"X-Mailbox-MD-Version": get_mailbox_md_version()}
@function_tool
def list_inbound_aliases() -> str:
"""List the private forwarding aliases you can send scans, PDFs, photos, and notes to."""
r = api_get("/inbound-forwarding-addresses")
aliases = r.json().get("forwarding_addresses", [])
if not aliases:
return "No forwarding aliases found."
return json.dumps(
[
{
"id": a["id"],
"label": a.get("label"),
"email": a.get("email"),
"source_type": a.get("source_type"),
"provider": a.get("provider"),
}
for a in aliases
],
indent=2,
)
@function_tool
def list_inbound_mail(limit: int = 10, include_drafting: bool = True) -> str:
"""List forwarded inbound mail context from existing addresses.
include_drafting adds reply-ready context for outbound follow-ups."""
include = "drafting" if include_drafting else ""
params = {"limit": limit}
if include:
params["include"] = include
r = api_get("/inbound", params=params)
items = r.json().get("inbound_mail", [])
if not items:
return "No inbound mail context found."
return json.dumps(items, indent=2)
@function_tool
def get_inbound_mail(inbound_id: str, include_files: bool = True) -> str:
"""Get one inbound mail item with summary, drafting context, and optional source files."""
include = ["drafting", "lineage"]
if include_files:
include.append("files")
r = api_get(
f"/inbound/{inbound_id}",
params={"include": ",".join(include)},
)
return json.dumps(r.json()["inbound_mail"], indent=2)
@function_tool
def list_physical_inbound(limit: int = 25) -> str:
"""List physical letters, flats, and parcels held at the assigned PMB."""
r = api_get("/inbound-items", params={"limit": limit})
return json.dumps(r.json().get("inbound_items", []), indent=2)
@function_tool
def request_open_and_scan(inbound_item_id: str, expected_version: int) -> str:
"""Request the only supported physical action after member approval."""
r = api_post(
f"/inbound-items/{inbound_item_id}/actions",
headers={"Idempotency-Key": f"open-scan-{inbound_item_id}-{expected_version}"},
json={
"action_type": "open_and_scan",
"expected_version": expected_version,
},
)
return json.dumps(r.json(), indent=2)
@function_tool
def get_physical_scan_results(inbound_item_id: str) -> str:
"""Read completed scans for one physical inbound item."""
r = api_get(f"/inbound-items/{inbound_item_id}/scans")
return json.dumps(r.json(), indent=2)
@function_tool
def send_letter(
recipient_name: str,
recipient_line1: str,
recipient_city: str,
recipient_state: str,
recipient_zip: str,
pdf_path: str,
recipient_line2: str = "",
recipient_country: str = "US",
mail_class: MailClass = "first_class",
return_name: str = "",
return_company: str = "",
return_line1: str = "",
return_line2: str = "",
return_city: str = "",
return_state: str = "",
return_zip: str = "",
agent_notes: str = "",
inbound_capture_id: str = "",
postal_mail_thread_id: str = "",
requires_approval: bool = False,
dry_run: bool = False,
) -> str:
"""Print and mail a physical letter. Supports USPS, FedEx, and UPS classes.
Use dry_run=True to preview cost without creating a record or charging."""
with open(pdf_path, "rb") as f:
r = api_post(
"/mail",
headers=mailbox_md_headers(),
files={"document": ("letter.pdf", f, "application/pdf")},
data={
"recipient_name": recipient_name,
"recipient_line1": recipient_line1,
"recipient_line2": recipient_line2,
"recipient_city": recipient_city,
"recipient_state": recipient_state,
"recipient_zip": recipient_zip,
"recipient_country": recipient_country,
"mail_class": mail_class,
"return_name": return_name,
"return_company": return_company,
"return_line1": return_line1,
"return_line2": return_line2,
"return_city": return_city,
"return_state": return_state,
"return_zip": return_zip,
"agent_notes": agent_notes,
"requires_approval": str(requires_approval).lower(),
"dry_run": str(dry_run).lower(),
"inbound_capture_id": inbound_capture_id,
"postal_mail_thread_id": postal_mail_thread_id,
},
)
payload = r.json()
if payload.get("dry_run"):
return json.dumps(payload, indent=2)
return json.dumps(payload["outbound_mail"], indent=2)
Single Agent
python
mail_agent = Agent(
name="Mail Ops",
instructions="""You manage inbound and outbound postal workflows.
Rules:
- Start with inbound context from forwarded aliases when the user mentions PDFs, scans, notices, or email-forwarded mail
- Use list_physical_inbound only for custody at the assigned PMB
- Request only open_and_scan, only after member approval, with the current item version
- Never infer another physical handling action
- Use draft_context from inbound mail when preparing a reply letter tied to a prior inbound item
- Use dry_run=True before expensive or sensitive outbound mail
- Use requires_approval=True for high-value, proof-sensitive, or operator-sensitive mail
- Choose certified when USPS proof of mailing and delivery is required; choose certified_return_receipt when electronic return-receipt evidence is required""",
tools=[
list_inbound_aliases,
list_inbound_mail,
get_inbound_mail,
list_physical_inbound,
request_open_and_scan,
get_physical_scan_results,
send_letter,
],
)
# Synchronous
result = Runner.run_sync(
mail_agent,
"Review forwarded inbound context, then report physical items that need member review."
)
print(result.final_output)
# Async
import asyncio
async def main():
result = await Runner.run(
mail_agent,
"Check forwarded inbound context and prepare a reply for any time-sensitive notice."
)
print(result.final_output)
asyncio.run(main())Handoff Between Agents
Use the Agents SDK’s handoff pattern to separate inbound review from outbound fulfillment.
python
from agents import Agent, Runner
outbound_specialist = Agent(
name="Outbound Specialist",
instructions="""You prepare outbound letters.
Use draft_context from inbound items when available.
Run dry_run=True first for sensitive or expensive mail, then send with
requires_approval=True when a human should confirm before fulfillment.""",
tools=[get_inbound_mail, send_letter],
)
inbound_triage = Agent(
name="Inbound Triage",
instructions="""You review digital inbound context and assigned-PMB physical mail.
Check forwarded inbound context first. For physical custody, request only
open_and_scan after member approval and include the current item version.
Hand off to Outbound Specialist when a reply letter should be drafted or mailed.""",
tools=[list_inbound_mail, get_inbound_mail, list_physical_inbound,
request_open_and_scan, get_physical_scan_results],
handoffs=[outbound_specialist],
)
result = Runner.run_sync(
inbound_triage,
"Review new inbound notices and mail a reply if one is required."
)
print(result.final_output)Safer Outbound Workflows
mailbox.bot already exposes approval and preview controls that fit well with agent workflows.
python
preview = send_letter(
recipient_name="County Clerk",
recipient_line1="123 Main St",
recipient_city="Sacramento",
recipient_state="CA",
recipient_zip="95814",
pdf_path="/tmp/reply.pdf",
mail_class="certified",
inbound_capture_id="00000000-0000-0000-0000-000000000000",
requires_approval=True,
dry_run=True,
)
# preview contains cost_breakdown, warnings, and normalized mail settings.
# After review, send again with dry_run=False to create the outbound record.