Email handoff for LiveKit voice agents

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.

Built for LiveKit Agents developers

Clean session-to-email handoff

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.

gated_send for voice-to-email trust

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.

Webhook-driven approval flow

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.

Mailboxes your agent actually owns

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.

Thread continuity across channels

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.


Get started in minutes

Send a session summary email at session end
python
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.

Tool call: compose follow-up email from within a session
python
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.

Check inbox for replies between sessions
python
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.

Webhook handler for approval events
python
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.


Step by step

1

Install LiveKit Agents and get a MultiMail API key

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.

bash
pip install livekit-agents

"cm"># Get your API key at https://app.multimail.dev
"cm"># Keys look like: mm_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
2

Provision a mailbox for your agent

Create a dedicated mailbox your agent will send from. You can use a multimail.dev subdomain or bring your own domain.

bash
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;
3

Add send_email as a tool in your AgentSession

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.

bash
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)]
)
4

Register a webhook endpoint for approval events

Set your webhook URL in the MultiMail dashboard so approval and rejection events POST to your server. Verify the signature on every request.

bash
"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;

Common questions

Does MultiMail block the LiveKit session while waiting for email approval?
No. The send_email call is async and returns immediately with a message ID and pending status. The session continues normally. Approval happens out-of-band via the MultiMail dashboard or API, and you receive the result via webhook — the realtime session path is never blocked.
Can my LiveKit agent receive replies to emails it sent?
Yes. Use the check_inbox endpoint to poll for inbound messages on your agent's mailbox. Replies from recipients land there and are readable via read_email and get_thread. You can pull these at the start of a new session to give the agent written context from the interval between sessions.
What oversight mode should I use for session summary emails?
gated_send is the right default for most voice-to-email use cases. The agent composes the email autonomously based on session content, but a human reviews it before delivery. This is appropriate when email content is generated from free-form conversation — the human review step catches errors or sensitive information the agent may have included incorrectly. Switch to monitored or autonomous once you have high confidence in your agent's output quality.
How do I pass session context into the email so reviewers can approve it efficiently?
Use the metadata field on the send_email request to attach your session ID, participant ID, or a session transcript reference. This metadata is visible in the MultiMail approval UI, so reviewers can look up the session before approving. The metadata is also included in approval webhook payloads so your server can correlate events back to sessions.
Can I use MultiMail without a custom domain?
Yes. Provision mailboxes on the multimail.dev domain — e.g., [email protected] — without any DNS configuration. Custom domains (with SPF, DKIM, and DMARC setup) are supported for production use where deliverability and brand identity matter.
Is there a Python SDK instead of raw HTTP calls?
The multimail-sdk Python package is not yet available for LiveKit integration — use httpx or aiohttp for async HTTP calls as shown in the code samples. The REST API is stable and fully documented at api.multimail.dev.

Explore more

The only agent email with a verifiable sender

Email infrastructure built for AI agents. Verifiable identity, graduated oversight, and a 38-tool MCP server. Formally verified in Lean 4.