npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@e2a/sdk

v2.5.0

Published

TypeScript SDK for e2a — email for AI agents

Readme

e2a TypeScript SDK

TypeScript/Node.js SDK for e2a — email for AI agents.

Install

npm install @e2a/sdk

Upgrading from 1.x to 2.0

Webhook-parsed emails now refuse to expose claim fields (sender, subject, textBody, …) until the HMAC signature is verified — email.sender throws UnverifiedEmailError instead of silently returning attacker-controllable data. The one-line fix is to switch client.parse(body)client.parseWebhook(body):

- const email = await client.parse(req.body);
+ const email = await client.parseWebhook(req.body);

parseWebhook reads the secret from E2A_WEBHOOK_SECRET; set it before upgrading. If you must inspect the payload before verifying, use email.unverifiedPayload. REST-fetched emails (client.getMessage) are unaffected — they're pre-verified via the bearer token. Full background in the PR.

Quick Start

Webhook (cloud agents)

Webhook payloads are HMAC-signed. The SDK gates field access behind verification — accessing email.sender, email.subject, etc. on an unverified payload throws UnverifiedEmailError. Use client.parseWebhook(...) to parse + verify in one call:

import { E2AClient } from "@e2a/sdk";

const client = new E2AClient(); // uses E2A_API_KEY env var

app.post("/webhook", async (req, res) => {
  let email;
  try {
    email = await client.parseWebhook(req.body); // reads E2A_WEBHOOK_SECRET
  } catch {
    return res.status(401).end();
  }
  console.log(`From: ${email.sender}, Subject: ${email.subject}`);
  await email.reply("Got it!");
  res.json({ ok: true });
});

Get a signing secret from the dashboard's Webhook secrets page (or POST /api/v1/users/me/signing-secrets). Set it as E2A_WEBHOOK_SECRET so parseWebhook picks it up automatically, or pass it explicitly: client.parseWebhook(body, "whsec_...").

Polling

import { E2AClient } from "@e2a/sdk";

const client = new E2AClient({
  apiKey: "e2a_...",
  agentEmail: "[email protected]",
});

// List unread messages
const { messages } = await client.getMessages({ status: "unread" });

// Read a specific message
const email = await client.getMessage(messages[0].messageId);
console.log(email.textBody);

// Reply
await email.reply("Thanks!");

Send a new email

await client.send(["[email protected]"], "Hello", "Hi from my agent!");

// With CC, BCC, and HTML body
await client.send(["[email protected]", "[email protected]"], "Hello", "Hi!", {
  htmlBody: "<p>Hi!</p>",
  cc: ["[email protected]"],
  bcc: ["[email protected]"],
});

API

new E2AClient(options?)

| Option | Type | Default | Description | |-------------|----------|------------------------|--------------------------------| | apiKey | string | E2A_API_KEY env var | Your API key | | agentEmail| string | E2A_AGENT_EMAIL env | Agent email address | | baseUrl | string | https://e2a.dev | API base URL | | timeout | number | 30000 | Request timeout in ms |

client.parseWebhook(body, secret?)Promise<InboundEmail>

Parse + HMAC-verify a webhook payload in one call. Reads E2A_WEBHOOK_SECRET if secret is omitted; throws on bad signature. Recommended entry point for webhook handlers.

client.parse(body)Promise<InboundEmail>

Deprecated since 2.2 — will be removed in 3.0. Use parseWebhook for webhook handlers, or call parseWebhook and read email.unverifiedPayload after the verification failure for inspection without verification. Calling parse logs a one-time deprecation warning to console.warn.

Parses a webhook payload (Buffer, string, or object) into an InboundEmail and returns it in the unverified state — accessing claim fields like sender or subject throws UnverifiedEmailError until email.verifySignature() succeeds.

client.getMessages(opts?)Promise<MessageList>

Fetch messages. Options: status ("unread", "read", "all"), pageSize, token.

client.getMessage(messageId)Promise<InboundEmail>

Fetch a single message with full content. Returns a pre-verified email (the bearer token already authenticated the channel) — no verifySignature step needed.

client.reply(messageId, body, opts?)Promise<SendResult>

Reply to a message. Options: htmlBody, replyAll, cc, bcc, conversationId, attachments.

client.send(to, subject, body, opts?)Promise<SendResult>

Send a new email. to is string[]. Options: htmlBody, cc, bcc, conversationId, attachments.

InboundEmail

| Property | Type | Description | |-----------------|-------------------|------------------------------------| | messageId | string | Unique message ID | | conversationId| string \| null | Thread/conversation ID (see below) | | sender | string | Sender email address | | recipient | string | Per-delivery target — your agent's address | | to | string[] | Parsed To: header — every address from the original message | | cc | string[] | Parsed Cc: header (empty when no CCs) | | replyTo | string[] | Parsed Reply-To: header — empty when absent (never falls back to sender). Useful when sender is a no-reply notifications address (Granola, GitHub CI bots, etc.) and the real correspondent is in Reply-To. | | subject | string | Email subject | | textBody | string | Plain-text body | | htmlBody | string \| null | HTML body | | attachments | Attachment[] | File attachments | | auth | AuthHeaders | Authentication headers | | isVerified | boolean | Whether sender identity is verified| | unverifiedPayload | WebhookPayload | Raw payload pre-verification — escape hatch for inspection; treat as untrusted | | reply(body) | method | Reply to this email |

All claim fields above (everything except auth, rawMessage, verified, isVerified, unverifiedPayload) are gated — accessing them on an unverified webhook payload throws UnverifiedEmailError. Call email.verifySignature(secret?) first (reads E2A_WEBHOOK_SECRET by default), or use client.parseWebhook(body) which combines parse + verify. email.unverifiedPayload is an escape hatch for inspection before verifying — treat its contents as untrusted.

Exported error class: UnverifiedEmailError extends Error.

Conversation threading

conversationId is an opaque string that lets your agent tie multiple emails to a single thread across the email boundary. Pass it on any send() / reply(), and e2a surfaces it on the recipient's inbound — whether the recipient is a human (via In-Reply-To threading) or another e2a agent (via a custom X-E2A-Conversation-Id header, honored only for same-platform mail so external senders cannot forge it).

client.on("message", async (email) => {
  const convId = email.conversationId ?? generateId();

  const reply = await buildReply(email);
  await email.reply(reply.text, {
    conversationId: convId,
    htmlBody: reply.html,
  });
});

When is conversationId populated?

| Inbound type | Sender passed conversationId? | What you see | |---|---|---| | First email from a human | n/a — humans don't pass it | nullassign one yourself if you want to thread | | Human reply to your prior outbound | n/a | The id you passed on your outbound | | Another e2a agent's new send | yes, recommended | The sender's asserted id | | Another e2a agent's new send | no | null | | Another e2a agent's reply | either way | Your earlier outbound's id unless the sender asserted another |

Rules of thumb:

  • Always pass conversationId when tagging an outbound as part of a known thread. It's the only way the recipient's webhook sees it.
  • On first contact from a human, assign a new id yourself before replying.
  • Handle null — it happens on first contact from humans and from external senders you haven't interacted with before.
  • conversationId is not a security boundary. For sender identity, check email.auth / email.isVerified.

Agent-to-agent threads

For e2a-to-e2a traffic, conversationId arrives on the very first message with no prior round trip required:

await agentA.send(["[email protected]"], "Can you handle this?", bodyText, {
  conversationId: "task-2026-04-19-7f3a",
});
// Agent B's webhook immediately sees conversationId="task-2026-04-19-7f3a"

WebSocket (real-time delivery for local agents)

Local-mode agents can receive notifications in real time over a WebSocket. No public URL needed; auth happens via the ?token= query parameter.

import { E2AClient } from "@e2a/sdk";

const client = new E2AClient({ apiKey: "e2a_..." });

for await (const notif of client.listen({ agentEmail: "[email protected]" })) {
  // The notification is lightweight metadata only — no body, no REST call.
  console.log(`From: ${notif.from}, Subject: ${notif.subject}`);

  // Fetch the full email when you actually want it.
  const detail = await client.api.getMessage(notif.recipient, notif.message_id);
  // ...
}

client.listen() returns a WSStream which is both an AsyncIterable<WSNotification> and an EventEmitter — pick whichever access pattern fits.

const stream = client.listen({ agentEmail: "[email protected]" });
stream.on("error", (err) => console.error("WS error:", err));
stream.on("close", (code, reason) => console.log("WS closed:", code, reason));

for await (const notif of stream) {
  // ...
}

// Call stream.close() to terminate iteration cleanly.

WSNotification mirrors the Python SDK's dataclass and the server's wire shape:

| Field | Type | Notes | |---|---|---| | message_id | string | Pass to client.api.getMessage(...) for the body | | from | string | Sender | | recipient | string | Per-delivery target (your agent's address) | | subject | string | | | received_at | string | RFC 3339 timestamp | | conversation_id | string? | Threading; absent on first contact |

Reconnects with exponential backoff (1s → 30s by default, configurable via maxBackoffMs). The protocol is server-to-client only; the client never sends application frames.

Lower-level WSListener

Prefer client.listen(). The underlying WSListener class is also exported for advanced use (e.g. wiring up a custom EventEmitter pattern without iteration), but most consumers should use client.listen().

License

Apache-2.0 — see LICENSE and NOTICE in the upstream repo.