Node.js Email SDK: Send and Receive in TypeScript
Use the Robotomail Node.js email SDK in TypeScript: list mailboxes, set a From name, send, read replies, keep threads, and keep API keys server-side.
John Joubert
Founder, Robotomail

Table of contents
The Robotomail Node.js email SDK gives a TypeScript agent its own mailbox: list the mailbox created at signup, send a message with a friendly From name, receive the reply, and reply back on the same thread. It runs server-side only, reads ROBOTOMAIL_API_KEY from the environment, and ships both ESM and CommonJS builds. This guide walks through the full loop and the mistakes that cost people an afternoon.
If you would rather call the HTTP API directly, we already covered that in sending email from Python and Node and in sending email from an AI agent. This post is about the typed SDK instead.
Requirements and install
You need Node.js 20.3 or newer. The SDK uses the platform fetch and AbortSignal behaviour that stabilised in that line, and we test against it.
Install from the tested v0.1.0 tag of the public robotomail/robotomail-node repository (MIT licensed):
npm install github:robotomail/robotomail-node#v0.1.0
The install specifier is a Git tag, but the package name in package.json is @robotomail/sdk, so that is what you import. Pinning the tag rather than a branch means your lockfile resolves to a commit we have actually run the test suite against.
Set the key in your environment, never in source:
export ROBOTOMAIL_API_KEY=rm_your_api_key
Two kinds of keys exist. Mailbox-scoped keys can send and read within one mailbox. Account-level operations and attachment uploads need a full-access key. The differences are spelled out in the authentication docs; if a call returns a permission error later in this guide, that is usually why.
First request: list the signup mailbox
Every account starts with one mailbox, so the fastest way to confirm your key and runtime is to list mailboxes and print their addresses.
import { Robotomail } from "@robotomail/sdk";
const mail = new Robotomail(); // reads ROBOTOMAIL_API_KEY
const { mailboxes } = await mail.listMailboxes();
console.log(mailboxes.map(box => box.fullAddress));
Output looks like:
[ 'shopping-agent@robotomail.co' ]
The constructor takes no arguments in the common case. It reads the key from process.env.ROBOTOMAIL_API_KEY and targets https://api.robotomail.com/v1. If you run multiple accounts in one process, pass the key explicitly instead of mutating the environment.
Hold on to the mailbox id from that response. Send, list and reply calls are all mailbox-scoped, and the id is what you pass to them.
ESM and CommonJS
The package exports both module formats, so the import style follows your project, not ours.
ESM, in a package with "type": "module" or in a .mts file:
import { Robotomail } from "@robotomail/sdk";
CommonJS, in a .cjs file or a package without "type": "module":
const { Robotomail } = require("@robotomail/sdk");
async function main() {
const mail = new Robotomail();
const { mailboxes } = await mail.listMailboxes();
console.log(mailboxes.map(box => box.fullAddress));
}
main().catch(err => {
console.error(err);
process.exit(1);
});
Two practical notes:
- Top-level
awaitis ESM only. In CommonJS wrap the work in anasync functionas above, or you will get a syntax error that looks like an SDK problem and is not. - For TypeScript, use
"module": "nodenext"and"moduleResolution": "nodenext"intsconfig.json. Older"moduleResolution": "node"settings resolve the package's type declarations inconsistently and you lose the typed request bodies, which is most of the reason to use an SDK.
Set the From name and send to your verified owner address
A send needs to, subject and bodyText. Add displayName to control the friendly From name that recipients see, so mail arrives as Shopping Agent <shopping-agent@robotomail.co> instead of a bare address.
import { Robotomail } from "@robotomail/sdk";
const mail = new Robotomail();
const { mailboxes } = await mail.listMailboxes();
const mailbox = mailboxes[0];
const sent = await mail.sendMessage(mailbox.id, {
to: ["you@yourcompany.com"], // your verified owner address
displayName: "Shopping Agent",
subject: "Order #A-4521 delivery update?",
bodyText: "Hi, checking on the status of order A-4521."
});
console.log(sent.id);
Send to your own verified owner address for this first test. It is the one recipient you know is deliverable before you have warmed anything up or attached a custom domain, and it lets you read the result in a client you already have open. Keep displayName in config rather than hardcoding it per call site, since it is part of your agent's identity and you will want to change it in one place. Exact method signatures and the full option list live in the TypeScript SDK reference.
bodyText is required. Send plain text first and add HTML once the loop works; plain text removes a whole class of rendering questions while you are debugging delivery.
Receive the reply
Reply now to that email from your own client. Two ways to pick it up.
Webhook (recommended for agents)
Register a webhook once, and inbound mail arrives as JSON with an event, a timestamp and a snake_case data object:
{
"event": "message.received",
"data": {
"message_id": "...",
"mailbox_id": "...",
"mailbox_address": "shopping-agent@robotomail.co",
"from": "you@yourcompany.com",
"subject": "Re: Order #A-4521 delivery update?",
"body_text": "Shipped Tuesday.",
"thread_id": "...",
"received_at": "2026-04-17T10:00:00.000Z"
}
}
A minimal Express handler:
import express from "express";
const app = express();
app.use(express.json());
app.post("/robotomail/inbound", (req, res) => {
const { event, data } = req.body;
if (event === "message.received") {
queue.add("handle-reply", {
threadId: data.thread_id,
messageId: data.message_id,
from: data.from,
bodyText: data.body_text
});
}
res.sendStatus(200); // acknowledge fast, process async
});
app.listen(3000);
Acknowledge with a 2xx quickly and do the agent work on a queue. Webhook registration, signature verification and retry behaviour are covered in the webhooks concept docs.
Polling
If you are writing a CLI, a cron job, or a local test harness, listing messages on the mailbox is simpler than exposing a public endpoint. There is also an events stream, but note that the SDK does not reconnect SSE for you; if you consume the stream, write your own reconnect with backoff, or use webhooks. We compared the tradeoffs in webhooks vs WebSockets.
Reply and preserve the thread
Threading is header work, not string matching on subjects. Pass the inbound message's RFC Message-ID as inReplyTo and the SDK sets the right headers so Gmail, Outlook and Apple Mail all file your reply under the existing conversation:
async function replyOnThread(mailboxId: string, inbound: {
message_id: string;
from: string;
subject: string;
}) {
return mail.sendMessage(mailboxId, {
to: [inbound.from],
displayName: "Shopping Agent",
subject: `Re: ${inbound.subject.replace(/^Re:\s*/i, "")}`,
bodyText: "Thanks, noting Tuesday as the ship date.",
inReplyTo: inbound.message_id
});
}
Drop inReplyTo and the recipient gets a new standalone conversation, which looks broken as soon as there are three or four turns. If you are building anything multi-turn, read how email threading works once and you will stop guessing.
Attachments follow the same pattern: upload with a full-access key, then pass the returned ids in attachments on the send.
Keep API keys out of browser bundles
This SDK is server-only. A Robotomail key can read a mailbox and send mail as your agent, so a leaked key is a leaked identity.
- Never prefix the variable with a client-exposed prefix.
NEXT_PUBLIC_,VITE_,PUBLIC_and friends are inlined into the bundle by design. Keep it as plainROBOTOMAIL_API_KEY. - Instantiate
Robotomailonly in server files: route handlers, server actions, background workers, queue consumers, cron entry points. Not in a React component that might render on the client. - Have the browser call your own endpoint, which authenticates your user and then calls Robotomail. Your API is the trust boundary.
- Use a mailbox-scoped key where the job only touches one mailbox. If it leaks, the blast radius is one mailbox instead of the account.
- Grep your build output before shipping:
grep -r "rm_" dist/ .next/. It takes two seconds and catches the accidental import.
More on isolating agent credentials in the agent email security guide.
Troubleshooting
Unauthorized on the first call. ROBOTOMAIL_API_KEY is not in the process environment. A .env file does nothing on its own under plain node; load it with node --env-file=.env script.js on Node 20.6+, or a loader of your choice.
Forbidden on account or attachment calls. You are using a mailbox-scoped key. Account operations and attachment uploads need a full-access key. See authentication.
ERR_REQUIRE_ESM or "Cannot use import statement outside a module". Module format mismatch in your project, not the SDK. Either add "type": "module" and use import, or use require from a CommonJS file.
Types resolve to any. Your tsconfig.json is on legacy module resolution. Switch to nodenext.
The reply started a new conversation. inReplyTo was missing or held an internal id rather than the RFC Message-ID from the inbound message.
A send timed out and you do not know if it went out. The SDK does not retry sends, deliberately, because a blind retry on an ambiguous POST is how agents send duplicates. List the mailbox's sent messages and check whether your message is there before resending.
Mail to a stranger bounced during testing. Send to your verified owner address until your sending setup and, if you use one, your custom domain are in place.
FAQ
Is the Node.js email SDK on npm?
Install it from the pinned Git tag shown above: npm install github:robotomail/robotomail-node#v0.1.0. The repository is public and MIT licensed. Check the SDK docs for current distribution before swapping in a registry command.
Does it work with TypeScript and JavaScript?
Both. Type declarations ship with the package, and the runtime builds cover ESM and CommonJS. TypeScript users should set module and moduleResolution to nodenext to get the typed request and response shapes.
Can I use the SDK in the browser or in an edge function?
Do not put the key in a browser bundle. Server runtimes are fine as long as the key stays in server-side environment variables and the SDK is only constructed in server code.
What does the SDK not do for me?
It does not retry sends and it does not reconnect an SSE stream. Both are intentional: retries risk duplicate mail, and reconnect policy belongs to your process supervisor. Use webhooks for inbound and inspect sent messages after an ambiguous send.
Ready to wire this up? Start with Robotomail, create your account and first mailbox, and keep the TypeScript SDK reference open while you build.
Give your AI agent a real email address
Create a mailbox, connect your agent and test a conversation. Send, receive and retrieve the thread through one API.
Related posts

How to Send Email From AI Agent: Python and TypeScript
Send email from AI agent code in Python or TypeScript: create a mailbox, one POST, plus the deliverability basics agents get wrong.
Read post
Python Email SDK: Build an Agent That Reads and Replies
Use the Robotomail Python email SDK to list mailboxes, send with a display name, and reply to inbound mail. Sync and async clients, with troubleshooting.
Read post
Go Email SDK: Build a Cancellable Email Worker
Use the Robotomail Go email SDK to list mailboxes, send with a display name, and run an SSE worker with context cancellation and manual reconnect.
Read post