When a live session ends, your agent shouldn't go silent. Wire MultiMail into your LiveKit worker to send auditable follow-ups, summaries, and confirmations after every conversation.
LiveKit Agents is a Python framework for building realtime voice and multimodal agents on top of LiveKit's media infrastructure. Agents run as workers that join rooms, listen for speech, invoke tools, and respond in voice — all within low-latency sessions optimized for live interaction.
The gap most voice agent builders hit: realtime sessions are ephemeral, but their outcomes need to persist. A scheduling agent books a call — someone needs a confirmation email. A support agent resolves an issue — the customer expects a written summary. A sales agent qualifies a lead — the CRM needs a follow-up sent.
MultiMail's REST API gives your LiveKit worker a direct path from session end to auditable email delivery. Use gated_send mode to let humans review outbound emails before they leave — a clean trust boundary between autonomous voice handling and permanent written communication.
LiveKit sessions are event-driven and short-lived. MultiMail's send_email endpoint accepts a POST at session end, decoupling your realtime logic from async email delivery without requiring a separate queue.
Voice agents move fast. Email is permanent. gated_send mode lets your agent compose the follow-up autonomously, but holds delivery until a human approves via the MultiMail dashboard or API — giving you automation speed with human accountability.
MultiMail fires approval webhooks to your server when a gated email is reviewed. Your LiveKit worker can subscribe to these events and trigger downstream actions — CRM updates, session archival, next-step scheduling — without polling.
Provision dedicated mailboxes via the create_mailbox endpoint — [email protected] or [email protected]. Replies from recipients land in check_inbox, so your agent can continue the thread after the voice session ends.
Use get_thread to pull the full email history when a contact rejoins a session. Your LiveKit agent can greet a returning caller with context from prior written exchanges — no separate memory layer needed.
import httpx
from livekit.agents import AgentSession, JobContext, WorkerOptions, cli
from livekit.agents.llm import ChatContext
MULTIMAIL_API_KEY = "mm_live_your_key_here"
MULTIMAIL_BASE = "https://api.multimail.dev"
async def send_session_summary(
recipient_email: str,
recipient_name: str,
summary: str,
session_id: str,
) -> dict:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{MULTIMAIL_BASE}/send_email",
headers={"Authorization": f"Bearer {MULTIMAIL_API_KEY}"},
json={
"from": "[email protected]",
"to": recipient_email,
"subject": "Summary from your session",
"body": f"Hi {recipient_name},\n\nHere&"cm">#039;s a summary of our conversation:\n\n{summary}\n\nReply to this email if you have questions.",
"oversight_mode": "gated_send",
"metadata": {"session_id": session_id},
},
)
response.raise_for_status()
return response.json()
async def entrypoint(ctx: JobContext):
await ctx.connect()
session = AgentSession()
@session.on("session_ended")
async def on_session_ended(summary_text: str):
# Extract contact info from your session metadata
participant = ctx.room.remote_participants.get(ctx.room.name)
if participant:
email = participant.metadata # store email in participant metadata
await send_session_summary(
recipient_email=email,
recipient_name=participant.name,
summary=summary_text,
session_id=ctx.room.name,
)
await session.start(ctx.room)
if __name__ == "__main__":
cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint))Hook into the session_ended event to post a follow-up email via the MultiMail REST API. This runs after LiveKit tears down the room, so it doesn't block the realtime path.
import httpx
from livekit.agents import AgentSession, JobContext, WorkerOptions, cli
from livekit.agents.llm import FunctionTool
from typing import Annotated
MULTIMAIL_API_KEY = "mm_live_your_key_here"
MULTIMAIL_BASE = "https://api.multimail.dev"
async def send_followup_email(
to: Annotated[str, "Recipient email address"],
subject: Annotated[str, "Email subject line"],
body: Annotated[str, "Plain text body of the follow-up email"],
) -> str:
"""Queue a follow-up email to the caller. Requires human approval before delivery."""
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{MULTIMAIL_BASE}/send_email",
headers={"Authorization": f"Bearer {MULTIMAIL_API_KEY}"},
json={
"from": "[email protected]",
"to": to,
"subject": subject,
"body": body,
"oversight_mode": "gated_send",
},
)
resp.raise_for_status()
data = resp.json()
return f"Email queued for approval. Message ID: {data[&"cm">#039;id']}"
async def entrypoint(ctx: JobContext):
await ctx.connect()
session = AgentSession(
tools=[FunctionTool.from_callable(send_followup_email)]
)
await session.start(ctx.room)
if __name__ == "__main__":
cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint))Expose send_followup_email as a tool your LLM can invoke mid-session. The agent can decide during the conversation that an email is warranted and queue it before the session ends.
import httpx
from livekit.agents import AgentSession, JobContext, WorkerOptions, cli
MULTIMAIL_API_KEY = "mm_live_your_key_here"
MULTIMAIL_BASE = "https://api.multimail.dev"
async def fetch_recent_emails(contact_email: str, limit: int = 5) -> list[dict]:
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{MULTIMAIL_BASE}/check_inbox",
headers={"Authorization": f"Bearer {MULTIMAIL_API_KEY}"},
params={
"mailbox": "[email protected]",
"from": contact_email,
"limit": limit,
},
)
resp.raise_for_status()
return resp.json().get("emails", [])
async def build_email_context(contact_email: str) -> str:
emails = await fetch_recent_emails(contact_email)
if not emails:
return ""
lines = ["Recent email exchanges with this contact:"]
for email in emails:
lines.append(f"- [{email[&"cm">#039;date']}] {email['subject']}: {email['preview']}")
return "\n".join(lines)
async def entrypoint(ctx: JobContext):
await ctx.connect()
# Get contact email from participant metadata
participant = next(iter(ctx.room.remote_participants.values()), None)
contact_email = participant.metadata if participant else None
email_context = ""
if contact_email:
email_context = await build_email_context(contact_email)
session = AgentSession()
# Inject email context into the initial chat context
if email_context:
session.chat_ctx.append(
role="system",
text=email_context,
)
await session.start(ctx.room)
if __name__ == "__main__":
cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint))When a participant rejoins, pull their email thread so the agent has context from any written follow-up that happened between sessions.
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
import httpx
app = FastAPI()
MULTIMAIL_WEBHOOK_SECRET = "whsec_your_secret_here"
MULTIMAIL_API_KEY = "mm_live_your_key_here"
MULTIMAIL_BASE = "https://api.multimail.dev"
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.post("/webhooks/multimail")
async def handle_multimail_webhook(request: Request):
payload = await request.body()
sig = request.headers.get("X-MultiMail-Signature", "")
if not verify_signature(payload, sig, MULTIMAIL_WEBHOOK_SECRET):
raise HTTPException(status_code=401, detail="Invalid signature")
event = await request.json()
event_type = event.get("type")
if event_type == "email.approved":
session_id = event["metadata"].get("session_id")
message_id = event["message_id"]
"cm"># Trigger downstream: update CRM, log to your system, notify operator
print(f"Email {message_id} approved for session {session_id}")
elif event_type == "email.rejected":
session_id = event["metadata"].get("session_id")
message_id = event["message_id"]
print(f"Email {message_id} rejected for session {session_id} — no delivery")
return {"ok": True}Receive MultiMail approval webhooks in a FastAPI server co-located with your LiveKit worker. When a human approves or rejects a queued email, trigger downstream logic.
Install the LiveKit Agents package and sign up for MultiMail to get a live API key. You'll use the key to authenticate REST calls from your worker.
pip install livekit-agents
"cm"># Get your API key at https://app.multimail.dev
"cm"># Keys look like: mm_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxCreate a dedicated mailbox your agent will send from. You can use a multimail.dev subdomain or bring your own domain.
curl -X POST https://api.multimail.dev/create_mailbox \
-H "Authorization: Bearer $MULTIMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d &"cm">#039;{
"address": "[email protected]",
"display_name": "Your Agent"
}&"cm">#039;Expose email sending as a callable tool. The LLM decides when to invoke it; MultiMail handles delivery in gated_send mode so humans approve before anything sends.
from livekit.agents import AgentSession
from livekit.agents.llm import FunctionTool
import httpx
from typing import Annotated
async def send_followup_email(
to: Annotated[str, "Recipient email"],
subject: Annotated[str, "Subject line"],
body: Annotated[str, "Email body"],
) -> str:
"""Send a follow-up email. Requires human approval before delivery."""
async with httpx.AsyncClient() as client:
resp = await client.post(
"https://api.multimail.dev/send_email",
headers={"Authorization": "Bearer $MULTIMAIL_API_KEY"},
json={
"from": "[email protected]",
"to": to,
"subject": subject,
"body": body,
"oversight_mode": "gated_send",
},
)
resp.raise_for_status()
return f"Email queued: {resp.json()[&"cm">#039;id']}"
session = AgentSession(
tools=[FunctionTool.from_callable(send_followup_email)]
)Set your webhook URL in the MultiMail dashboard so approval and rejection events POST to your server. Verify the signature on every request.
"cm"># Register your webhook endpoint
curl -X POST https://api.multimail.dev/webhooks \
-H "Authorization: Bearer $MULTIMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d &"cm">#039;{
"url": "https://your-worker-host.com/webhooks/multimail",
"events": ["email.approved", "email.rejected", "email.delivered"]
}&"cm">#039;Email infrastructure built for AI agents. Verifiable identity, graduated oversight, and a 38-tool MCP server. Formally verified in Lean 4.