Skip to content
Mailcycle
Get started
JavaScript and TypeScript email SDK

The JavaScript email API for Node.js, Deno, Bun and Workers.

Receive email in JavaScript or TypeScript with a typed client and no dependencies. Create an address, await the next message, read it, reply to it. Mail is stored encrypted and opened in your own process with WebCrypto, so the same code runs on Node, Deno, Bun, Cloudflare Workers and in the browser.

Install the TypeScript package
npm install mailcycle-sdk

mailcycle-sdk on npm · Node.js 20 or later · Apache 2.0 · Free tier, no card

Quickstart

Receive and send email in JavaScript and TypeScript.

Sign in, create an address, wait for a message and answer it. That is the whole loop, and it is the same loop whether the mail is a sign-up code, a customer or a receipt.

  1. 1Install mailcycle-sdk and set MAILCYCLE_PHRASE to your account's recovery phrase.
  2. 2Create an address with mc.addresses.create(). It receives mail straight away.
  3. 3Wait for the next message with mc.messages.waitFor(), or take it from a webhook.
  4. 4Reply with mc.messages.send(), threaded under the original.
The full TypeScript guide
TypeScript: create an address, wait for mail, reply
import { Mailcycle } from 'mailcycle-sdk';

const mc = await Mailcycle.signIn({ phrase: process.env.MAILCYCLE_PHRASE! });

// A new address that receives mail, sealed to your key.
const address = await mc.addresses.create({ prefix: 'signup', label: 'Test run 42' });
console.log(address.emailAddress); // signup-x7k2@...

// Wait for the verification email, decrypted here.
const message = await mc.messages.waitFor({
  inboxId: address.id,
  subject: 'verification',
  timeoutMs: 60_000,
});
console.log(message.subject, message.text);

// Reply in the same thread.
await mc.messages.send({
  from: address.emailAddress,
  to: message.replyTo,
  subject: `Re: ${message.subject}`,
  text: 'Thanks, confirmed.',
  replyTo: message,
});
What it does

The whole email API, from TypeScript.

Everything the Mailcycle console can do is in the package: addresses, inbound mail, sending, webhooks, events, API keys and devices.

Create email addresses from TypeScript

mc.addresses.create() mints a new address on a shared domain, or on your own domain from Scale, and it receives mail at once. Give it a prefix and a label sealed on your machine.

Receive email and wait for it

mc.messages.waitFor() listens on the event stream and returns the next matching message, filtered by sender or subject, opened and ready to read. Without a stream it checks every five seconds.

Read and parse incoming email

mc.messages.list() and .attachment() return the sender, subject, plain text, the original HTML, a safe HTML with remote images and trackers removed, and every attachment as bytes.

Send email and reply in the thread

mc.messages.send() sends from an address your account owns, with up to ten files. Pass the message you are answering and the threading headers are filled in.

Inbound email webhooks

Register a URL and every message.received event is POSTed to it, signed with HMAC-SHA256 and retried. verifyWebhookSignature() checks the signature in one line.

A live event stream

mc.events.stream() carries the same events over a WebSocket the moment they happen, and reconnects after a drop. Events carry ids, never content.

Email testing

Test email verification in Playwright, Vitest and Jest.

Create an address in a beforeEach, fill your sign-up form with Playwright, then await mc.messages.waitFor() and read the one-time code or magic link from message.text. If nothing arrives in time the promise rejects with code timeout, so the test fails with a reason rather than hanging.

  • A real address on a real domain, so the mail goes through the same path your users' does.
  • One call blocks until the message lands, filtered by subject or sender, with a timeout you set.
  • Text, HTML and attachments come back parsed, so pulling out a code or a link is a regular expression.
  • Mail received from the moment you start waiting counts, even if it lands before the stream is up.
Inbound email webhooks

Handle incoming email in Next.js route handlers, Express, Hono and Cloudflare Workers.

Rather than polling, let the mail come to you. A webhook POSTs each event to your URL the moment it happens, signed so you can prove it came from Mailcycle, and retried if your server is down.

Events carry ids, never content. Fetch the message by id and it is opened in your code, exactly as it would be otherwise. Webhooks are on Operator and above.

Webhooks in the docs
A fetch-style route handler (Next.js, Hono, Workers)
import { verifyWebhookSignature } from 'mailcycle-sdk';

export async function POST(request: Request) {
  const raw = await request.text(); // the body exactly as it arrived
  const signature = request.headers.get('Mailcycle-Signature');
  if (!(await verifyWebhookSignature(SECRET, raw, signature))) {
    return new Response(null, { status: 401 });
  }

  const event = JSON.parse(raw);
  if (event.type === 'message.received') {
    const message = await mc.messages.get(event.payload.messageId);
    await handle(message.subject, message.text);
  }
  return new Response(null, { status: 204 });
}
No mail server

Instead of Nodemailer plus an IMAP client.

Nodemailer sends through an SMTP transport you provide, and receiving means adding an IMAP client and a real mailbox, or running an SMTP server of your own and parsing what it accepts with a MIME parser.

This package does both directions over HTTPS with fetch. An address is created on demand, mail is pushed to your webhook or awaited with a promise, and messages come back parsed, with safeHtml ready to render. It has no dependencies, so it works where Node's net module does not, such as Cloudflare Workers.

It is not a bulk sender. If you need newsletters or high volumes of transactional mail, use a sending service for that; Mailcycle is for addresses your code owns and the mail that comes back to them.

End-to-end encrypted

Mail is opened in your TypeScript code, not on our servers.

Each address has a key pair derived from your recovery phrase. Incoming mail is sealed to its public key before anything is written down, and the package derives the private key on your machine to open it.

How the encryption works
  • Signing in proves you hold the key by answering a challenge, so the phrase never crosses the wire.
  • Bodies, attachments, address labels and API key names are all sealed. The server stores ciphertext.
  • Mail sealed by one SDK opens in every other, and in the app, because they share one published format.
  • The package is checked against Mailcycle's published test vectors, including the cases that must be refused.
  • Because the server cannot read mail, there is no server-side search. Search runs where the mail is opened.
Use cases

What developers build with the TypeScript SDK.

It runs in Express and Next.js apps, Cloudflare Workers, serverless functions, CLIs and browser tools: anywhere TypeScript code needs an email address of its own.

End-to-end tests for sign-up and login

Verification emails, one-time passcodes, magic links, password resets and invitations, checked by a real test in Playwright, Vitest and Jest.

A support or orders inbox your code answers

Give an address to a queue, read what customers send, and reply from the address they wrote to, threaded under their message.

Receipts, invoices and alerts as data

Point suppliers, monitoring and other services at an address and pull the text and attachments straight into your own records.

An address per customer, job or device

Mint addresses on demand and keep each stream of mail separate, with its own label, and delete one when the job is done.

Building an AI agent? It can have an inbox too, through the Mailcycle MCP server, without writing any TypeScript at all.

The package

JavaScript and TypeScript SDK at a glance.

Maintained by Mailcycle, the team that runs the API.

The Mailcycle TypeScript SDK at a glance
Packagemailcycle-sdk
Registrynpm
Install withnpm, pnpm, Yarn or Bun
NeedsNode.js 20 or later
AlsoAlso any runtime with fetch and WebCrypto: Deno, Bun, Cloudflare Workers and browsers. No dependencies.
LicenceApache 2.0, free to use in your own applications
APIJSON over HTTPS at api.mailcycle.email
GuideThe TypeScript guide in the docs

What each plan unlocks. Free receives mail. A signed-in session sends from Personal up. API keys and webhooks start at Operator, and sending from an API key at Scale. See pricing.

Questions

JavaScript and TypeScript email SDK questions.

Run npm install mailcycle-sdk, sign in with Mailcycle.signIn({ phrase }), and create an address with mc.addresses.create(). Mail sent to it is stored encrypted; mc.messages.list() or await mc.messages.waitFor() returns it decrypted with the sender, subject, text, HTML and attachments parsed. No SMTP server, no IMAP mailbox.

Yes. It needs only fetch and WebCrypto and has no dependencies, so it runs on Node.js 20 or later, Deno, Bun, Cloudflare Workers and in browsers. On Node 20 the event stream needs a WebSocket class passed in as an option; Node 22 and later have one built in.

Yes. The package ships its own type definitions, so messages, addresses, events and errors are typed in your editor. It works the same from plain JavaScript.

Call await mc.addresses.create({ prefix: 'signup' }). It returns the new address and its id, and the address receives mail straight away. Behind it is a single POST to the Mailcycle API; the SDK also derives the address's key on your machine so the mail that arrives can be opened. Delete it with mc.addresses.delete() when you are done.

Ethereal is a fake SMTP service: it catches the mail your app sends and never delivers it, which is good for previewing outgoing messages. Mailcycle gives your tests real addresses on real domains, so mail travels the same path a user's does, from any sender, and your code can wait for it, read it and reply. Use Ethereal to look at what you send, and Mailcycle to receive what comes back.

Call await mc.messages.send({ from, to, subject, text }) with an address your account owns. It returns the Message-ID it went out with. Passing replyTo: message threads a reply under the original. Sending from a signed-in session works from the Personal plan, and from an API key on Scale and above.

The package is free and open source under the Apache 2.0 licence. The Mailcycle Free plan costs nothing and needs no card: 3 email addresses and 3,000 messages received a month, receive only. Sending starts on Personal at $7 a month, API keys and webhooks on Operator at $15, and sending from an API key on Scale at $19.

No. Incoming mail is sealed to the address's public key before it is stored, and the private key is derived from your recovery phrase inside your TypeScript process. The server holds ciphertext it cannot open. Mail you send leaves in plain text so the recipient's server can read it, and the copy kept in Sent is sealed like the rest.

Call mc.addresses.create() at the start of the run and delete the address at the end, which frees its place on your plan. Each address receives mail as soon as it exists. This is for testing your own product and for mail meant for you: registering accounts on other services in bulk, or against their terms, breaks the Acceptable Use Policy.

No. Mailcycle is inbound email infrastructure, and sending is there to answer what arrives. Sending is capped at 60 messages an hour per account and 20 an hour per device or agent, with a daily limit set by the plan, and the Acceptable Use Policy rules out bulk, marketing and outreach mail.

The API is plain JSON over HTTPS at api.mailcycle.email, so anything can call it. Message content comes back encrypted, though, and the package is what derives the keys and opens it. Without an SDK you would implement the published encryption format yourself.

Give your TypeScript code an email address.

Create an account, create an address, and read what arrives. The free tier needs no card.