Send email with Express
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
Install
Get Express and the mails.ai SDK. The SDK is an ES module, so the second command sets the project to use
import.npm install express @mailsai/sdk npm pkg set type=moduleSet your API key
Put the key in an environment variable. The SDK reads
MAILS_API_KEYon its own, so the key never appears in your code.export MAILS_API_KEY="mk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Send from a route
fromnames 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 (themanagescope) and your plan has room for another; withoutmanagethe send answers403 insufficient_scope.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:
node server.js curl -X POST http://localhost:3000/sendreply@test.mails.aiis 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.Receive replies on a webhook
mails.ai signs every event it posts to your endpoint with an
X-Mails-Signatureheader.mails.webhooks.verifychecks that header against the exact bytes that arrived and returns the event, ornullwhen the check fails. For 24 hours after you rotate the signing secret the header carries twov1signatures, soverifyEventchecks each one. Give this one routeexpress.raw, so the handler gets those bytes and not a parsed object. Add it toserver.jsaboveapp.listen:// 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(); });quarantinedistruewhen the message scored as a likely prompt injection. Skip those, and answer with a2xxwithin 5 seconds: any other answer, or none, counts as a failed delivery and is tried again, 3 attempts in all.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 themanagescope.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_secretthat starts withwhsec_and is shown once. Set it asMAILS_WEBHOOK_SECRETwhere the server runs.
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?