Installation

terminal
npm install @deadsimple/email

Quick Start

Create a client, make an inbox, and send your first email:

quickstart.ts
import { DeadSimple } from "@deadsimple/email";

const client = new DeadSimple("dse_your_api_key");
const inbox = await client.inboxes.create({ displayName: "My Agent" });

await client.messages.send({
  inboxId: inbox.inbox_id,
  to: "user@example.com",
  subject: "Hello from my agent",
  textBody: "This email was sent by an AI agent.",
});

Resources

Every API endpoint is available as a typed method on the client:

Resource Methods
client.inboxes create, list, get, delete, bulkCreate, bulkDelete
client.messages send, list, get, reply, forward
client.threads list, get
client.webhooks create, list, get, update, delete (with inboxIds scope)
client.apiKeys create (with inboxIds scope), list, get, delete
client.calendar create, list, listAll, get, update, delete, invite, rsvp, feed, rotateFeed
client.metrics get
client.domains add, list, verify, delete
client.identity signIn, enrollKey, listKeys, deleteKey, connections
client.usage get

Reading Messages

List messages in an inbox and read individual message content:

read-messages.ts
// List recent messages
const messages = await client.messages.list({
  inboxId: "inb_abc123",
  limit: 10,
});

// Read full content of a specific message
const message = await client.messages.get({
  inboxId: "inb_abc123",
  messageId: messages.data[0].message_id,
});

console.log(message.subject, message.text_body);

Identity: sign an agent in to other apps

Any app that offers Sign in with Dead Simple accepts an inbox as a login over standard OpenID Connect. Hand the SDK the app's authorization URL and it completes the flow with the API key the client already holds:

identity.ts
// authorizationUrl is the app's "Sign in with Dead Simple" link
// (its /authorize URL with PKCE, state and scopes already set).
const result = await client.identity.signIn(inbox.inboxId, authorizationUrl)
console.log(result.redirectTo, result.finalUrl, result.status)

// Agents that should not hold a full API key: enrol a P-256 public key once,
// then sign a short-lived ES256 assertion at sign-in time.
const key = await client.identity.enrollKey(inbox.inboxId, { publicKey: agentPublicKeyPem, name: "worker-7" })
await client.identity.listKeys(inbox.inboxId)        // shows lastUsedAt per key
await client.identity.deleteKey(inbox.inboxId, key.kid)

// Every app the inbox has signed in to, with scope and sign-in counts
await client.identity.connections(inbox.inboxId)

Pay per request. The SDK needs an API key, but the API itself does not: priced routes answer 402 with no credentials and accept USDC over x402 or a Stripe card over MPP inline, and the first paid inbox returns an API key you can then pass to new DeadSimple({ apiKey }). Prices, headers and a @x402/fetch example are on the pay per request page.

Calendar: events, invitations, RSVPs and a feed

Every inbox has a calendar. Create events, send them as real iCalendar invitations, answer invitations that arrived by email, and hand a human the subscription URL. Times need a zone: a trailing Z, an offset, or timezone next to a local time.

calendar.ts
// Create, then invite. Nothing is sent until invite.
const event = await client.calendar.create(inboxId, {
  title: "Kickoff",
  start: "2026-09-22T11:00:00", timezone: "America/New_York",
  duration_minutes: 30,
  attendees: ["dana@example.com"],
})
const sent = await client.calendar.invite(inboxId, event.event_id, { textBody: "Agenda to follow." })

// Reschedule: update raises sequence, invite again sends a revision, not a duplicate
await client.calendar.update(inboxId, event.event_id, { start: "2026-09-23T11:00:00", timezone: "America/New_York" })
await client.calendar.invite(inboxId, event.event_id)

// Invitations that arrived by email are already on the calendar (source === "inbound")
const { events } = await client.calendar.list(inboxId, { status: "confirmed" })
for (const ev of events) {
  if (ev.source === "inbound") await client.calendar.rsvp(inboxId, ev.event_id, "accepted", { comment: "See you then." })
}

// Subscription URLs for Google Calendar, Apple Calendar or Outlook; rotate if the URL leaks
const feed = await client.calendar.feed(inboxId)   // feed.feed_url, feed.webcal_url
await client.calendar.rotateFeed(inboxId)

// Every event on every inbox the key can see
await client.calendar.listAll({ from: "2026-09-21", to: "2026-09-27" })

Metrics: a time series for the account or an inbox

client.metrics.get() wraps GET /v1/metrics. Each bucket carries sent, received, bounced, complained, opened, clicked, webhook_deliveries, webhook_failures and injection_flagged, plus totals over the window. Windows span at most 92 days; hourly buckets are available for windows of seven days or less.

metrics.ts
// Last 30 days, daily buckets (the defaults)
const m = await client.metrics.get()
console.log(m.totals.sent, m.totals.received, m.totals.bounced)

// One week, hourly, for two inboxes
const week = await client.metrics.get({
  from: "2026-09-10", to: "2026-09-17", interval: "hour",
  inboxIds: [supportId, billingId],
})
for (const bucket of week.series) {
  if (bucket.webhook_failures || bucket.injection_flagged) console.log(bucket.start, bucket)
}

Inbox-scoped API keys and webhooks

Pass inboxIds when creating an API key and the key can see and act on those inboxes only: any other inbox of the account answers 404, account-wide listings (inboxes, messages, threads, metrics) are filtered to them, and the key cannot create inboxes or mint a key with more reach than it has. Webhooks take the same field and fire only for events on those inboxes.

scoping.ts
// A key for one agent, limited to its own inbox
const key = await client.apiKeys.create({
  name: "support-agent", permissions: ["read", "write"], inboxIds: [supportId],
})
console.log(key.key)   // shown once

// A webhook that only hears that inbox
const hook = await client.webhooks.create({
  url: "https://agents.example.com/hooks/support",
  events: ["message.received", "calendar.event.created"],
  inboxIds: [supportId],
})

// Change the scope later; inboxIds: [] makes it account-wide again
await client.webhooks.update(hook.webhook_id, { inboxIds: [supportId, billingId] })
await client.webhooks.get(hook.webhook_id)

Webhook Verification

The SDK includes a helper to verify webhook signatures in your Express or Next.js routes:

webhook.ts
import { verifyWebhook } from "@deadsimple/email";

export async function POST(req: Request) {
  const body = await req.text();
  const signature = req.headers.get("x-dse-signature");

  const event = verifyWebhook(body, signature, process.env.DSE_WEBHOOK_SECRET);

  if (event.type === "message.received") {
    console.log("New email from:", event.data.from);
  }

  return new Response("ok");
}

Vercel AI SDK Integration

The package includes a deadsimple/ai subpath export with Zod-validated tool definitions for the Vercel AI SDK. This lets you give any AI model email capabilities in one line:

ai-tools.ts
import { getTools } from "@deadsimple/email/ai";
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";

const tools = getTools("dse_your_api_key");

const result = await generateText({
  model: openai("gpt-4o"),
  tools,
  prompt: "Send a welcome email to new-user@example.com",
});

See the full Vercel AI SDK integration docs for streaming, multi-step agents, and raw function definitions.

Configuration

The client accepts the API key directly, or reads it from the DSE_API_KEY environment variable:

config.ts
// Explicit key
const client = new DeadSimple("dse_your_api_key");

// Or from environment (reads DSE_API_KEY automatically)
const client = new DeadSimple();

// Custom base URL (for self-hosted or testing)
const client = new DeadSimple("dse_key", {
  baseUrl: "https://api.your-domain.com",
});

Ready to build?

Create a free account and start integrating in minutes.

API Reference Get Started Free