Email infrastructure for Claude agents built with the Anthropic SDK

Wire MultiMail's REST API as Claude tool calls. Send, read, and approve emails with structured inputs, gated oversight modes, and a complete audit trail below the model layer.


The Anthropic SDK exposes Claude's tool use interface as a first-class API primitive. You define tools as JSON schemas, Claude decides when to call them, and your application executes the calls and feeds results back. This loop maps directly onto email operations: send_email, check_inbox, read_email, reply_email, and get_thread all fit naturally as tool definitions.

What the SDK does not provide is enforcement policy. When Claude calls send_email, nothing in the SDK validates the recipient, checks whether a human approved the send, or records the decision for audit. MultiMail adds those controls below the model layer — as a REST API your tool executor calls — so Claude's email capability is governed regardless of what the model decides.

The integration is additive. You keep your existing messages loop, system prompt, and tool definitions. MultiMail replaces the outbound email call inside your tool executor with one that enforces the oversight_mode you configured for that agent mailbox.

Built for Anthropic Agent SDK developers

Policy enforcement below the model

Claude decides when to send email. MultiMail enforces who it can send to, whether a human must approve, and what gets logged. These controls live in the API layer, not in the system prompt, so they cannot be overridden by prompt injection or model drift.

Graduated oversight modes

Configure gated_send so every outbound send waits for human approval, or switch to autonomous once the agent has earned trust. The same agent code runs in all modes — only the mailbox configuration changes.

Structured tool call inputs

MultiMail's REST endpoints accept the same JSON fields you define in Claude's tool input_schema. No translation layer needed — the object Claude generates goes directly into the request body.

Inbound email as tool results

check_inbox and read_email return structured JSON that fits cleanly as tool_result content. Claude can parse subject, sender, body, and thread_id without custom parsing logic in your application.

Audit trail for every tool call

Every send_email, reply_email, and decide_email call is logged with the originating agent, timestamp, recipients, and approval state. Required for SOC 2 and GDPR Article 5 accountability obligations.


Get started in minutes

Define MultiMail tools for Claude
typescript
import Anthropic from '@anthropic-ai/sdk';

export const emailTools: Anthropic.Tool[] = [
  {
    name: 'send_email',
    description: 'Send an email from the agent mailbox. Returns pending status in gated_send mode.',
    input_schema: {
      type: 'object' as const,
      properties: {
        to: { type: 'string', description: 'Recipient email address' },
        subject: { type: 'string', description: 'Email subject line' },
        body: { type: 'string', description: 'Plain text email body' },
        reply_to_id: { type: 'string', description: 'Email ID to thread this as a reply (optional)' }
      },
      required: ['to', 'subject', 'body']
    }
  },
  {
    name: 'check_inbox',
    description: 'List recent emails in the agent mailbox.',
    input_schema: {
      type: 'object' as const,
      properties: {
        limit: { type: 'number', description: 'Max emails to return (default 20)' },
        unread_only: { type: 'boolean', description: 'Return only unread emails' }
      }
    }
  },
  {
    name: 'read_email',
    description: 'Read the full content of a specific email by ID.',
    input_schema: {
      type: 'object' as const,
      properties: {
        email_id: { type: 'string', description: 'Email ID from check_inbox' }
      },
      required: ['email_id']
    }
  }
];

Register send_email, check_inbox, and read_email as Claude tool definitions with input schemas that match MultiMail's REST API parameters.

Agent tool execution loop
typescript
import Anthropic from '@anthropic-ai/sdk';
import { emailTools } from './tools';

const client = new Anthropic();

async function callMultiMail(
  toolName: string,
  input: Record<string, unknown>
): Promise<unknown> {
  const res = await fetch(`https:"cm">//api.multimail.dev/${toolName}`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(input)
  });
  if (!res.ok) {
    const err = await res.json() as { error: string };
    throw new Error(`MultiMail ${toolName} failed: ${err.error}`);
  }
  return res.json();
}

async function runEmailAgent(task: string): Promise<string> {
  const messages: Anthropic.MessageParam[] = [
    { role: 'user', content: task }
  ];

  while (true) {
    const response = await client.messages.create({
      model: 'claude-opus-4-7',
      max_tokens: 4096,
      system: 'You are an email assistant. Use tools to manage the inbox and send emails on behalf of the user.',
      tools: emailTools,
      messages
    });

    if (response.stop_reason === 'end_turn') {
      const textBlock = response.content.find(b => b.type === 'text');
      return textBlock?.type === 'text' ? textBlock.text : '';
    }

    if (response.stop_reason !== 'tool_use') break;

    messages.push({ role: 'assistant', content: response.content });

    const toolResults: Anthropic.ToolResultBlockParam[] = await Promise.all(
      response.content
        .filter((b): b is Anthropic.ToolUseBlock => b.type === 'tool_use')
        .map(async (block) => {
          try {
            const result = await callMultiMail(
              block.name,
              block.input as Record<string, unknown>
            );
            return {
              type: 'tool_result' as const,
              tool_use_id: block.id,
              content: JSON.stringify(result)
            };
          } catch (err) {
            return {
              type: 'tool_result' as const,
              tool_use_id: block.id,
              content: String(err),
              is_error: true
            };
          }
        })
    );

    messages.push({ role: 'user', content: toolResults });
  }

  return '';
}

Run a full Claude tool use loop that dispatches tool calls to MultiMail's REST API and feeds structured responses back to the model.

Gated approval workflow
typescript
import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();

interface PendingEmail {
  id: string;
  to: string;
  subject: string;
  body: string;
  created_at: string;
}

async function mm(endpoint: string, body: Record<string, unknown>) {
  const res = await fetch(`https:"cm">//api.multimail.dev/${endpoint}`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(body)
  });
  return res.json();
}

async function reviewAndDecide(email: PendingEmail): Promise<void> {
  const response = await client.messages.create({
    model: 'claude-opus-4-7',
    max_tokens: 512,
    system:
      'You are a compliance reviewer. Approve professional, on-topic emails. ' +
      'Reject anything that looks like spam, contains sensitive PII, or would violate CAN-SPAM. ' +
      'Respond with JSON only: {"decision": "approve" | "reject", "reason": string}',
    messages: [
      {
        role: 'user',
        content:
          `To: ${email.to}\nSubject: ${email.subject}\n\n${email.body}`
      }
    ]
  });

  const text =
    response.content.find(b => b.type === 'text')?.text ?? '{}';
  const { decision, reason } = JSON.parse(text) as {
    decision: 'approve' | 'reject';
    reason: string;
  };

  await mm('decide_email', { email_id: email.id, decision, reason });
  console.log(`${email.id}: ${decision} — ${reason}`);
}

async function runApprovalLoop(): Promise<void> {
  const data = await mm('list_pending', {}) as { pending: PendingEmail[] };
  console.log(`${data.pending.length} emails pending review`);
  for (const email of data.pending) {
    await reviewAndDecide(email);
  }
}

runApprovalLoop();

Poll list_pending for held sends, use a second Claude call to review each one, then call decide_email to approve or reject. Pair with gated_send or gated_all oversight modes.

Streaming with tool use
typescript
import Anthropic from '@anthropic-ai/sdk';
import { emailTools } from './tools';

const client = new Anthropic();

async function streamEmailAgent(task: string): Promise<void> {
  const messages: Anthropic.MessageParam[] = [
    { role: 'user', content: task }
  ];

  while (true) {
    const stream = client.messages.stream({
      model: 'claude-opus-4-7',
      max_tokens: 4096,
      tools: emailTools,
      messages
    });

    for await (const event of stream) {
      if (
        event.type === 'content_block_delta' &&
        event.delta.type === 'text_delta'
      ) {
        process.stdout.write(event.delta.text);
      }
    }

    const finalMessage = await stream.finalMessage();
    if (finalMessage.stop_reason !== 'tool_use') break;

    messages.push({ role: 'assistant', content: finalMessage.content });

    const toolResults: Anthropic.ToolResultBlockParam[] = await Promise.all(
      finalMessage.content
        .filter((b): b is Anthropic.ToolUseBlock => b.type === 'tool_use')
        .map(async (block) => {
          const result = await fetch(
            `https:"cm">//api.multimail.dev/${block.name}`,
            {
              method: 'POST',
              headers: {
                Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}`,
                'Content-Type': 'application/json'
              },
              body: JSON.stringify(block.input)
            }
          ).then(r => r.json());

          return {
            type: 'tool_result' as const,
            tool_use_id: block.id,
            content: JSON.stringify(result)
          };
        })
    );

    messages.push({ role: 'user', content: toolResults });
  }
}

Stream Claude's reasoning to the user while still executing MultiMail tool calls synchronously when the stream resolves to a tool_use stop reason.


Step by step

1

Install the Anthropic SDK

Add the Anthropic TypeScript SDK to your project.

bash
npm install @anthropic-ai/sdk
2

Create an agent mailbox

Create a mailbox for your agent and set the oversight mode. Use gated_send to require human approval on all outbound sends while reads remain autonomous.

bash
curl -X POST https://api.multimail.dev/create_mailbox \
  -H &"cm">#039;Authorization: Bearer $MULTIMAIL_API_KEY...' \
  -H &"cm">#039;Content-Type: application/json' \
  -d &"cm">#039;{"address": "[email protected]", "oversight_mode": "gated_send"}'
3

Define email tools as Anthropic tool schemas

Register MultiMail endpoints as Anthropic tool definitions. The input_schema properties map directly to the JSON fields each endpoint accepts.

bash
const emailTools: Anthropic.Tool[] = [
  {
    name: &"cm">#039;send_email',
    description: &"cm">#039;Send an email from the agent mailbox.',
    input_schema: {
      type: &"cm">#039;object',
      properties: {
        to: { type: &"cm">#039;string' },
        subject: { type: &"cm">#039;string' },
        body: { type: &"cm">#039;string' }
      },
      required: [&"cm">#039;to', 'subject', 'body']
    }
  },
  {
    name: &"cm">#039;check_inbox',
    description: &"cm">#039;List recent emails.',
    input_schema: { type: &"cm">#039;object', properties: {} }
  }
];
4

Wire the tool executor in your messages loop

When stop_reason is 'tool_use', dispatch each tool_use block to the matching MultiMail endpoint and push the results back as tool_result blocks.

bash
if (response.stop_reason === &"cm">#039;tool_use') {
  messages.push({ role: &"cm">#039;assistant', content: response.content });

  const toolResults = await Promise.all(
    response.content
      .filter((b): b is Anthropic.ToolUseBlock => b.type === &"cm">#039;tool_use')
      .map(async (block) => {
        const result = await fetch(
          `https://api.multimail.dev/${block.name}`,
          {
            method: &"cm">#039;POST',
            headers: {
              Authorization: `Bearer ${process.env.MULTIMAIL_API_KEY}`,
              &"cm">#039;Content-Type': 'application/json'
            },
            body: JSON.stringify(block.input)
          }
        ).then(r => r.json());

        return {
          type: &"cm">#039;tool_result' as const,
          tool_use_id: block.id,
          content: JSON.stringify(result)
        };
      })
  );

  messages.push({ role: &"cm">#039;user', content: toolResults });
}

Common questions

Which Claude models support tool use with MultiMail?
All Claude 3 and Claude 4 models support tool use. claude-opus-4-7 and claude-sonnet-4-6 are recommended for production agents that need reliable tool call generation. claude-haiku-4-5-20251001 is suited for high-volume, lower-cost tasks like inbox triage or tag_email classification. All three call MultiMail's REST endpoints identically.
How does gated_send affect what Claude sees in the tool result?
When a mailbox is configured with oversight_mode gated_send, send_email returns immediately with a status of 'pending' and an email_id. The email is held until a human calls decide_email. Claude receives the pending status as a normal tool_result and can continue reasoning — for example, it can tell the user 'Your message is queued for approval.' The held email appears in list_pending.
Can I use the Anthropic streaming API alongside MultiMail tool calls?
Yes. Stream text delta events for Claude's reasoning output, then call stream.finalMessage() to get the completed message. If stop_reason is 'tool_use', extract the tool_use blocks, dispatch them to MultiMail, and push tool_result blocks into messages for the next iteration. The streaming and non-streaming paths handle tool execution identically.
How do I test without delivering real emails?
Use a test key (mm_test_...) as your MULTIMAIL_API_KEY. All send_email calls in test mode return a success response and log the operation, but no email is delivered. list_pending and decide_email still function so you can test gated approval flows end-to-end without real sends.
Does MultiMail filter the email content Claude generates?
MultiMail enforces structural policy — oversight mode, recipient allowlists, rate limits — but does not filter body content. Content review is your application's responsibility. The gated_send mode exists specifically to give a human or a second Claude call the opportunity to review body content via decide_email before delivery.
What compliance requirements does this integration help satisfy?
The audit log satisfies GDPR Article 5(2) accountability requirements by recording who processed what personal data and when. Oversight modes support CAN-SPAM opt-out enforcement by routing unsubscribe-triggered sends through a human approval step before delivery. The gated flow also supports HIPAA minimum-necessary requirements when agents handle patient correspondence.

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.