Integration
TypeScript SDK
Promise-based TypeScript client with full type definitions for Node.js applications and AI agents.
Installation
npm install @deadsimple/email
Quick Start
Create a client, make an inbox, and send your first email:
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:
// 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:
// 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.
// 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.
// 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.
// 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:
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:
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:
// 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", });