Skip to main content

Send email with Next.js

One route handler sends, one receives signed reply events. App Router, server-side only.
Use this pre-built prompt to get started faster.
Open in Cursor

Prerequisites

Before you start, you need:

  • A mails.ai API key. A test key (mk_test_…) runs everything on this page and sends no real email.
  • A Next.js app that uses the App Router.

Guide

  1. Install

    Get the mails.ai TypeScript SDK.

    Terminal
    npm install @mailsai/sdk
  2. Add your API key

    Next.js loads .env.local into process.env on the server, and the SDK reads MAILS_API_KEY from there. Leave off the NEXT_PUBLIC_ prefix: the key must never reach the browser.

    MAILS_API_KEY=mk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  3. Send from a route handler

    from names the agent that sends the message: an agent with that name is used if it exists, and created if it does not when the key may create agents (the manage scope) and your plan has room for another; without manage the send answers 403 insufficient_scope. mails reads the key when it is first used, so importing it is safe in a build that has no key.

    app/api/send/route.ts
    import { mails, MailsError } from "@mailsai/sdk";
    
    export async function POST() {
      try {
        const sent = await mails.send({
          from: "hello",
          to: "reply@test.mails.ai",
          subject: "Welcome aboard",
          body: "Thanks for signing up. Reply any time and I will read it.",
        });
        return Response.json({ id: sent.id, status: sent.status, from: sent.from });
      } catch (error) {
        if (error instanceof MailsError) {
          return Response.json({ code: error.code, message: error.message }, { status: error.status });
        }
        throw error;
      }
    }

    Start the app and call the route:

    Terminal
    curl -X POST http://localhost:3000/api/send

    reply@test.mails.ai is an address of ours that answers within about a second, so there is already a reply waiting for the next step. How the test address works.

  4. Receive replies on a webhook

    mails.ai signs every event it posts to your endpoint with an X-Mails-Signature header. mails.webhooks.verify checks that header against the exact bytes that arrived and returns the event, or null when the check fails. For 24 hours after you rotate the signing secret the header carries two v1 signatures, so verifyEvent checks each one. Read the body with request.text(): a parsed and re-serialized body no longer matches the signature.

    app/api/webhooks/mails/route.ts
    import { mails } from "@mailsai/sdk";
    
    // Accepts the event when any v1 signature in the header matches the secret.
    function verifyEvent(body: string, header: string, secret: string) {
      const parts = header.split(",");
      const t = parts.find((p) => p.startsWith("t=")) ?? "";
      for (const v1 of parts.filter((p) => p.startsWith("v1="))) {
        const event = mails.webhooks.verify(body, `${t},${v1}`, secret);
        if (event) return event;
      }
      return null;
    }
    
    export async function POST(request: Request) {
      const body = await request.text();
      const signature = request.headers.get("x-mails-signature") ?? "";
      const event = verifyEvent(body, signature, process.env.MAILS_WEBHOOK_SECRET ?? "");
    
      if (!event) {
        return new Response("bad signature", { status: 400 });
      }
    
      if (event.type === "reply.received" && !event.quarantined) {
        const reply = event.data;
        console.log(reply.subject, reply.body_text_excerpt, event.injection_score);
        // Hand the reply to your agent here.
      }
    
      return new Response(null, { status: 200 });
    }

    quarantined is true when the message scored as a likely prompt injection. Skip those, and answer with a 2xx within 5 seconds: any other answer, or none, counts as a failed delivery and is tried again, 3 attempts in all.

  5. Register the webhook

    Deploy the app, then tell mails.ai where the route lives. The address must be https:// and reachable from the internet. This call needs a key with the manage scope.

    Terminal
    curl https://api.mails.ai/v1/webhooks \
      -H "Authorization: Bearer $MAILS_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://your-app.com/api/webhooks/mails",
        "event_types": ["reply.received", "message.received"]
      }'

    The answer carries a signing_secret that starts with whsec_ and is shown once. Save it as MAILS_WEBHOOK_SECRET in your host’s environment settings.

Working on your own machine, where mails.ai cannot reach a webhook? Ask for the events instead: GET /v1/events?event_type=reply.received returns the same events, and Events & streaming shows how to keep a live connection open.

Next steps

Was this page helpful?