Skip to main content

Send email with Express

One Express server: a route that sends, and a webhook route that receives signed reply events.
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.
  • Node.js 18 or newer.

Guide

  1. Install

    Get Express and the mails.ai SDK. The SDK is an ES module, so the second command sets the project to use import.

    Terminal
    npm install express @mailsai/sdk
    npm pkg set type=module
  2. Set your API key

    Put the key in an environment variable. The SDK reads MAILS_API_KEY on its own, so the key never appears in your code.

    export MAILS_API_KEY="mk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  3. Send from a route

    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.

    server.js
    import express from "express";
    import { mails, MailsError } from "@mailsai/sdk";
    
    const app = express();
    
    app.post("/send", async (req, res) => {
      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.",
        });
        res.status(201).json({ id: sent.id, status: sent.status, from: sent.from });
      } catch (error) {
        if (error instanceof MailsError) {
          res.status(error.status).json({ code: error.code, message: error.message });
          return;
        }
        throw error;
      }
    });
    
    app.listen(3000, () => {
      console.log("Listening on http://localhost:3000");
    });

    Start the server and call the route from a second terminal:

    Terminal
    node server.js
    curl -X POST http://localhost:3000/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. Give this one route express.raw, so the handler gets those bytes and not a parsed object. Add it to server.js above app.listen:

    server.js
    // Accepts the event when any v1 signature in the header matches the secret.
    function verifyEvent(body, header, secret) {
      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;
    }
    
    app.post("/webhooks/mails", express.raw({ type: "application/json" }), (req, res) => {
      const event = verifyEvent(
        req.body.toString("utf8"),
        req.get("x-mails-signature") ?? "",
        process.env.MAILS_WEBHOOK_SECRET ?? "",
      );
    
      if (!event) {
        res.status(400).send("bad signature");
        return;
      }
    
      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.
      }
    
      res.status(200).end();
    });

    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 server, 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/webhooks/mails",
        "event_types": ["reply.received", "message.received"]
      }'

    The answer carries a signing_secret that starts with whsec_ and is shown once. Set it as MAILS_WEBHOOK_SECRET where the server runs.

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?