Mails.ai with the Vercel AI SDK

We wrapped mails.ai as AI SDK tools, and our Next.js route streams each step of the agent’s email work to the page.

Mira OkaforFrontend Engineer, Lichenford

Install and wire it up

Install with npm install ai @ai-sdk/anthropic @mailsai/sdk zod. Then wrap mails.ai as tool() definitions:

import { generateText, isStepCount, tool } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
import { createClient } from "@mailsai/sdk";

const client = createClient({ apiKey: process.env.MAILS_API_KEY! });
const hello = client.agent("hello");

const sendEmail = tool({
  description: "Send an email from the hello agent.",
  inputSchema: z.object({
    to: z.string().email(),
    subject: z.string(),
    body: z.string(),
  }),
  execute: async ({ to, subject, body }) => {
    const result = await hello.send({ to, subject, body });
    return { send_id: result.id, classifier_score: result.classifier_score };
  },
});

const listThreads = tool({
  description: "List recent replies to the hello agent.",
  inputSchema: z.object({
    since_days: z.number().default(1),
  }),
  execute: async ({ since_days }) => {
    return (await hello.listReplies({ since: new Date(Date.now() - since_days * 86_400_000).toISOString() })).data;
  },
});

const result = await generateText({
  model: anthropic("claude-sonnet-5-5"),
  tools: { sendEmail, listThreads },
  stopWhen: isStepCount(5),
  prompt: "Email user@example.com a follow-up about Tuesday's demo.",
});

Guide

Tool definitions for serverless agents

TS SDK · 5 min read · ai-sdk.dev

Vercel AI SDK is a widely used TypeScript AI toolkit for web apps on Next.js, SvelteKit and Nuxt. Provider-agnostic (Anthropic, OpenAI, Google, Mistral, all behind one API). Wire mails.ai as tool() definitions and your serverless agent can send + read email as part of generateText or streamText flows.

Why Vercel AI SDK + mails.ai

The combination is opinionated for the most common modern web-app shape: Next.js + Vercel + Anthropic + serverless. mails.ai bolts onto that stack as a tool layer:

  • In-app AI chat that can email. User chats with your AI assistant in your Next.js app; the assistant offers to email a summary or follow-up; the SDK fires mails.send via the tool definition.
  • Server-Action-driven email automations. User clicks “email our sales team about my use case” in your form; a Server Action invokes generateText with the mails tool, drafts the email, sends it, returns confirmation.
  • Stream UI for live email composition. Use streamText with tool calls to show the agent drafting and sending in real-time, with the UI updating as tool results stream back.
  • Node runtime. The mails.ai SDK runs on Node 18+ (fetch under the hood); edge runtime untested.

Setup

  1. Install dependencies. npm install ai @ai-sdk/anthropic @mailsai/sdk zod
  2. Set environment variables. MAILS_API_KEY for mails, ANTHROPIC_API_KEY for the model. Vercel project settings handle both.
  3. Define tool() wrappers. The pattern in the install card. Wrap send, threads.list and me() as needed by your use case.
  4. Bind to generateText / streamText. Pass the tools object as the tools parameter. The model picks tools by name during the conversation.

Common patterns

Server Action with tool-driven email:

// app/actions/contact.ts
"use server";

import { generateText, isStepCount, tool } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
import { mails } from "@mailsai/sdk";

const hello = mails.agent("hello", { apiKey: process.env.MAILS_API_KEY! });

const sendEmail = tool({
  description: "Send an email from hello.",
  inputSchema: z.object({
    to: z.string().email(),
    subject: z.string(),
    body: z.string(),
  }),
  execute: async ({ to, subject, body }) => {
    const r = await hello.send({ to, subject, body });
    return { send_id: r.id };
  },
});

export async function emailSalesTeam(userMessage: string) {
  const { text } = await generateText({
    model: anthropic("claude-sonnet-5-5"),
    tools: { sendEmail },
    stopWhen: isStepCount(5),
    prompt: `User said: "${userMessage}". Email sales@ourcompany.com with a brief summary asking them to follow up.`,
  });
  return text;
}

Streaming chat with email tool:

// app/api/chat/route.ts
import { createUIMessageStreamResponse, streamText, toUIMessageStream, tool } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
import { mails } from "@mailsai/sdk";

const hello = mails.agent("hello", { apiKey: process.env.MAILS_API_KEY! });

export async function POST(req: Request) {
  const { messages } = await req.json();
  const result = streamText({
    model: anthropic("claude-sonnet-5-5"),
    messages,
    tools: {
      sendEmail: tool({
        description: "Send an email from hello",
        inputSchema: z.object({ to: z.string().email(), subject: z.string(), body: z.string() }),
        execute: async (args) => hello.send(args),
      }),
    },
  });
  return createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream }) });
}

Inbound webhook handling

For inbound replies, set up a Next.js Route Handler that receives the mails.ai webhook and processes the typed event:

// app/api/webhooks/mails-inbound/route.ts
import { NextRequest, NextResponse } from "next/server";
import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

export async function POST(req: NextRequest) {
  const event = await req.json();
  if (event.type !== "message.received" && event.type !== "reply.received") {
    return NextResponse.json({ ok: true }); // only inbound mail carries a score
  }

  // Hold for a person at the door: quarantined, not scanned (no score), or 0.5 and up
  if (event.quarantined || typeof event.injection_score !== "number" || event.injection_score >= 0.5) {
    await flagForReview(event);
    return NextResponse.json({ ok: true, action: "held_for_review" });
  }

  // Use AI SDK to draft a response based on intent
  if (event.intent === "schedule_demo") { // needs classify_inbound: true on the agent
    const { text } = await generateText({
      model: anthropic("claude-sonnet-5-5"),
      prompt: `Draft a calendar-confirmation reply for: ${event.data.extracted_text}`,
    });
    // Send it via mails.ai
    // ...
  }

  return NextResponse.json({ ok: true });
}

Security considerations

  • Server-side only. Tool executions run server-side (generateText/streamText in Server Actions or Route Handlers). The mails.ai key never reaches the browser.
  • Verify webhook signatures. Mails.ai webhooks are signed; verify the signature in your Route Handler before processing the event. Reject mismatches. For 24 hours after you rotate the secret a delivery carries two v1 signatures, so accept a match on either.
  • Per-route mails.ai key. If you have multiple agents (different brand voices, different use cases), use Vercel project environment variables to scope keys per-route.

Compare against the Anthropic SDK setup for a more direct integration when not on Vercel AI SDK, or OpenAI Agents SDK for the framework-driven approach.

Read next: Mails.ai with the Anthropic SDK and Mails.ai with the OpenAI Agents SDK.

Where to go next

The quickstart, both SDKs and the full API reference.

  • Quickstart

    From an API key to a sent message and its reply event, over plain REST.

    Read the quickstart
  • SDKs

    @mailsai/sdk for TypeScript and mailsai for Python, thin wrappers over the same API.

    Read the SDK docs
  • API reference

    Every endpoint, with the exact request and response fields.

    Check the reference

Questions developers ask after wiring this up.

Is there an MCP-client integration like the Anthropic SDK has?
Yes. Vercel AI SDK is provider-agnostic — it normalizes Anthropic, OpenAI, Google, Mistral, etc., behind a single API. Connect the hosted server, https://api.mails.ai/mcp, with createMCPClient (@ai-sdk/mcp), or wrap @mailsai/sdk in tool(). The tool() helper pattern is the SDK’s idiomatic way to add tools today and works identically with any model provider.
Does this work with streamText?
Yes. Tools fire during streaming the same way they fire in generateText.
Edge runtime or Node runtime — does it matter?
Use Node. The @mailsai/sdk client runs on Node 18+ (fetch under the hood); edge runtime untested.
Can I use this with Server Actions in Next.js App Router?
Yes. Define the tools in a 'use server' file and call generateText from a Server Action invoked by your form/button. The mails.* tool calls happen server-side; the Action returns the agent’s final response to the client. Common pattern for chat interfaces where the user types a request and the agent emails on their behalf.

“Replies come back as events with an injection score already on them. We deleted a whole layer of parsing code the week we switched, and we gate on the quarantine flag, so our agent never sees the ones that look like attacks.”

Tomás VargaStaff Engineer, Cinderjay

“Moving our agent onto our own domain was a few DNS records at the registrar. No nameserver move, and our existing mail kept working. Replies to the agent still come back to its inbox, threaded with the message they answer.”

Ishani VaidyaCTO, Sedgequay

“The 422 on cold outreach is the feature I didn’t know I wanted. An agent can’t talk itself into emailing strangers.”

Marcus FeldFounder, Marrowkite

“Our tests send to the test address and wait for the real reply, so the whole loop is covered before a customer ever writes in. It answers in about a second, which keeps the suite fast.”

Ana Lucía RíosEngineering Lead, Gorsefinch

“Adding the MCP server was one JSON block. Claude Code could send and read its own inbox a minute later.”

Jonah AbramsDeveloper, Wickerjay

“A reputation score per agent tells us exactly which one needs attention, instead of one number for the whole account. It comes from each agent’s own replies, bounces and complaints, and we read it from the API.”

Mei Lin ZhouOperations, Larchwhistle

“Sends and replies have separate allowances, so a busy inbox never eats our sending quota. Pricing was the easy part.”

Kwabena AdomakoFounder, Oxbowlark

“Half our agents are LangGraph in Python and half are Node. Both SDKs make the same calls, so the team doesn’t have to think about it.”

Sofia BrandtEngineer, Moss & Ladder

“Signed webhooks, retries with backoff and an event id to dedupe on. The boring plumbing, done properly.”

Nikhil BhonsleBackend Lead, Thimblecrow

“We signed up, made a key and sent our first message without talking to anyone, and the free tier never asked for a card. That’s how infrastructure should feel.”

Frieda WesselFounder, Ploverwick Studio

Give your first agent an inbox

Free covers 3,000 emails and 3,000 inbound replies a month, with no card. Upgrade when your agents get busy.

Get your API key