Email infrastructure for agents — set up an inbox and send your first email in 30 seconds. Programmatic inboxes (~1 line), consistent threads, custom domains, attachments, and structured data.
npm install commune-aiEmail infrastructure for agents — set up an inbox and send your first email in 30 seconds. Programmatic inboxes (~1 line), consistent threads, setup and verify custom domains, send and receive attachments, structured data extraction.
``bash`
npm install commune-ai
---
`ts
import express from "express";
import { CommuneClient, createWebhookHandler, verifyCommuneWebhook } from "commune-ai";
const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
const handler = createWebhookHandler({
verify: ({ rawBody, headers }) => {
const signature = headers["x-commune-signature"];
const timestamp = headers["x-commune-timestamp"];
if (!signature || !timestamp) return false;
return verifyCommuneWebhook({
rawBody,
timestamp,
signature,
secret: process.env.COMMUNE_WEBHOOK_SECRET!,
});
},
onEvent: async (message, context) => {
// Example inbound payload:
// message = {
// channel: "email",
// conversation_id: "thread_id",
// participants: [{ role: "sender", identity: "user@example.com" }],
// content: "Can you help with pricing?"
// }
// --- Run your agent here (1–2 line LLM call) ---
const prompt = Reply to: ${message.content};
const agentReply = await llm.complete(prompt); // replace with your LLM client
// Email reply (same thread)
const sender = message.participants.find(p => p.role === "sender")?.identity;
if (!sender) return;
await client.messages.send({
channel: "email",
to: sender,
text: agentReply,
conversation_id: message.conversation_id,
domainId: context.payload.domainId,
inboxId: context.payload.inboxId,
});
},
});
const app = express();
app.post("/commune/webhook", express.raw({ type: "/" }), handler);
app.listen(3000, () => console.log("listening on 3000"));
`
bash
npm install commune-ai
`---
Unified inbox (what your webhook receives)
Every inbound email arrives in this shape:`ts
export interface UnifiedMessage {
channel: "email";
message_id: string;
conversation_id: string; // email thread
participants: { role: string; identity: string }[];
content: string;
metadata: { ... };
}
`---
API key (required)
All /api/* requests require an API key. Create one in the dashboard and reuse it in your client.`bash
export COMMUNE_API_KEY="your_key_from_dashboard"
export COMMUNE_WEBHOOK_SECRET="your_webhook_secret"
``ts
const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
`---
Attachments
Send and receive email attachments with secure storage and temporary download URLs.$3
Upload attachments first, then use the attachment ID when sending emails.`ts
import fs from 'fs';// 1. Upload attachment (base64 encoded)
const fileBuffer = fs.readFileSync('invoice.pdf');
const base64Content = fileBuffer.toString('base64');
const { attachment_id } = await client.attachments.upload(
base64Content,
'invoice.pdf',
'application/pdf'
);
// 2. Send email with attachment
await client.messages.send({
to: 'customer@example.com',
subject: 'Your invoice',
text: 'Please find your invoice attached.',
attachments: [attachment_id],
domainId: 'your-domain-id',
});
`$3
Attachments are available through:
- Email events in the incoming webhook
- Metadata of semantic search resultsUse the
attachment_id to access the attachment.`ts
// Get attachment metadata
const attachment = await client.attachments.get("att_123");
console.log(attachment.filename, attachment.size);// Get download URL (expires in 1 hour)
const { url } = await client.attachments.get("att_123", { url: true });
// Use the URL to download or display the file
// Custom expiration (2 hours)
const { url } = await client.attachments.get("att_123", { url: true, expiresIn: 7200 });
`$3
`ts
const handler = createWebhookHandler({
onEvent: async (message, context) => {
const { attachments } = context.payload;
if (attachments && attachments.length > 0) {
for (const att of attachments) {
console.log(Attachment: ${att.filename} (${att.size} bytes));
// Get download URL
const { url } = await client.attachments.get(att.attachment_id, { url: true });
// Download the file
const response = await fetch(url);
const buffer = await response.arrayBuffer();
// Process the file...
}
}
},
});
`---
Semantic Search
Commune provides powerful semantic search capabilities to help your agent find relevant conversations and context. The search is powered by embeddings and vector similarity, allowing natural language queries.$3
`ts
import { CommuneClient } from "commune-ai";
const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });// Search across all conversations in an organization
const results = await client.searchConversations(
"customer asking about refund policy",
{ organizationId: "org_123" }
);
// Search with inbox filter
const inboxResults = await client.searchConversations(
"shipping delays",
{
organizationId: "org_123",
inboxIds: ["inbox_1", "inbox_2"]
}
);
// Search by participant
const userResults = await client.searchConversations(
"account upgrade request",
{
organizationId: "org_123",
participants: ["user@example.com"]
}
);
// Search with date range
const dateResults = await client.searchConversations(
"feature request",
{
organizationId: "org_123",
startDate: "2026-01-01",
endDate: "2026-02-01"
}
);
`$3
By default, all conversations are automatically indexed. You can also manually index conversations:`ts
// Index a single conversation
await client.indexConversation("org_123", {
id: "conv_123",
subject: "Product Inquiry",
content: "Customer asking about product features",
metadata: {
subject: "Product Inquiry",
organizationId: "org_123",
inboxId: "inbox_1",
domainId: "domain_1",
participants: ["customer@example.com"],
threadId: "thread_1",
timestamp: new Date()
}
});// Batch index multiple conversations
await client.indexConversationBatch("org_123", [
{
id: "conv_1",
subject: "Support Request",
content: "Customer needs help with login",
metadata: {
subject: "Support Request",
organizationId: "org_123",
inboxId: "support_inbox",
domainId: "domain_1",
participants: ["customer@example.com"],
threadId: "thread_1",
timestamp: new Date()
}
},
// ... more conversations
]);
`$3
Search results include relevance scores and metadata:`ts
interface SearchResult {
id: string; // Conversation ID
score: number; // Similarity score (0-1)
metadata: {
subject: string; // Email subject
organizationId: string;
inboxId: string;
domainId: string;
participants: string[];
threadId: string;
timestamp: Date;
direction?: 'inbound' | 'outbound'; // Email direction (sent or received)
attachmentIds?: string[]; // Attachment IDs in this conversation
hasAttachments?: boolean; // Whether conversation has attachments
attachmentCount?: number; // Number of attachments
};
}
`$3
`ts
// Search for conversations with attachments
const results = await client.searchConversations(
"invoice with receipt",
{ organizationId: "org_123" }
);// Filter results that have attachments
const withAttachments = results.filter(r => r.metadata.hasAttachments);
// Access attachments from search results
for (const result of withAttachments) {
for (const attachmentId of result.metadata.attachmentIds || []) {
const { url, filename } = await client.attachments.get(attachmentId, { url: true });
console.log(
Download: ${filename} - ${url});
}
}
`Spam Protection
Commune includes built-in spam detection and protection to keep your agent safe from malicious emails and prevent abuse.$3
All incoming emails are automatically analyzed for spam patterns:
- Content analysis (spam keywords, suspicious patterns)
- URL validation (broken links, phishing detection)
- Sender reputation tracking
- Domain blacklist checkingSpam emails are automatically rejected before reaching your webhook, protecting your agent from:
- Phishing attempts
- Malicious links
- Mass spam campaigns
- Fraudulent content
$3
Commune also protects your sending reputation:
- Rate limiting per organization tier
- Content validation for outbound emails
- Burst detection for mass mailing
- Automatic blocking of spam-like contentThis ensures your domain maintains high deliverability and isn't flagged by email providers.
$3
If a spam email gets through, you can report it:`ts
import { CommuneClient } from "commune-ai";
const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });// Report a message as spam
await client.reportSpam({
message_id: "msg_123",
reason: "Unsolicited marketing email",
classification: "spam" // or "phishing", "malware", "other"
});
`The system learns from reports and automatically blocks repeat offenders.
Context (conversation state)
Commune stores conversation state so your agent can respond with context.`ts
import { CommuneClient } from "commune-ai";
const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });// Thread history (email thread)
const thread = await client.messages.listByConversation(message.conversation_id, {
order: "asc",
limit: 50,
});
// All messages from a user
const userHistory = await client.messages.list({
sender: "user@example.com",
limit: 25,
});
// All messages in a specific inbox
const inboxMessages = await client.messages.list({
inbox_id: "i_xxx",
channel: "email",
limit: 50,
});
`---
Email handling
The UnifiedMessage shape works for email messages.`ts
import express from "express";
import { CommuneClient, createWebhookHandler } from "commune-ai";// Hosted API is default. If self-hosted, pass { baseUrl: "https://your-api" }
const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
const handler = createWebhookHandler({
onEvent: async (message, context) => {
const sender = message.participants.find(p => p.role === "sender")?.identity;
if (!sender) return;
await client.messages.send({
channel: "email",
to: sender,
text: "Got it — thanks for the message.",
conversation_id: message.conversation_id,
domainId: context.payload.domainId,
inboxId: context.payload.inboxId,
});
},
});
const app = express();
app.post("/commune/webhook", express.raw({ type: "/" }), handler);
app.listen(3000);
`---
---
Setup instructions (dashboard-first)
Domain setup and inbox creation are done in the Commune dashboard. You then copy the
IDs into your code.$3
Use a subdomain like agents.yourcompany.com for deliverability and isolation.$3
The dashboard guides you through DNS (SPF/DKIM/MX) and verification.$3
Each inbox represents an agent address (e.g. support@agents.yourcompany.com).$3
The webhook payload already includes:
- domainId (e.g. d_xxx)
- inboxId (e.g. i_xxx)Use them when replying:
`ts
await client.messages.send({
channel: "email",
to: "user@example.com",
text: "Thanks — replying in thread.",
conversation_id: message.conversation_id,
domainId: context.payload.domainId,
inboxId: context.payload.inboxId,
});
`> The SDK also supports programmatic domain/inbox creation, but the dashboard
> flow is the primary path for most teams.
$3
When you configure the inbox webhook in the dashboard, Commune shows a webhook secret.
Store it as:`bash
export COMMUNE_WEBHOOK_SECRET="your_webhook_secret"
`Use it in the
verify function shown above.$3
Use the dashboard to create an API key, then set it as:`bash
export COMMUNE_API_KEY="your_key_from_dashboard"
`---
Structured extraction (per inbox)
You can attach a JSON schema to a specific inbox so Commune extracts structured data from inbound emails.$3
In Dashboard → Inboxes, open an inbox and add a Structured Extraction schema. Save it and enable extraction.$3
`ts
await client.inboxes.setExtractionSchema({
domainId: "domain-123",
inboxId: "inbox-456",
schema: {
name: "invoice_extraction",
description: "Extract invoice details",
enabled: true,
schema: {
type: "object",
properties: {
invoiceNumber: { type: "string" },
amount: { type: "number" },
dueDate: { type: "string" }
},
required: ["invoiceNumber", "amount"],
additionalProperties: false
}
}
});
`$3
The webhook payload includes the structured output when extraction is enabled.`ts
const handler = createWebhookHandler({
onEvent: async (message, context) => {
const extracted = context.payload.extractedData
|| message.metadata?.extracted_data
|| null; if (extracted) {
// use structured fields in your agent workflow
console.log("Extracted:", extracted);
}
},
});
`> Tip: Keep your schema minimal (only the fields you need). You can evolve it over time.
$3
`ts
import { CommuneClient, createWebhookHandler } from "commune-ai";const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
await client.inboxes.setExtractionSchema({
domainId: "domain-123",
inboxId: "inbox-456",
schema: {
name: "invoice_extraction",
enabled: true,
schema: {
type: "object",
properties: {
invoiceNumber: { type: "string" },
amount: { type: "number" },
vendor: { type: "string" }
},
required: ["invoiceNumber", "amount"],
additionalProperties: false
}
}
});
const handler = createWebhookHandler({
onEvent: async (message, context) => {
const extracted = context.payload.extractedData;
if (!extracted) return;
console.log("Invoice:", extracted.invoiceNumber);
console.log("Amount:", extracted.amount);
await processInvoice(extracted);
},
});
`---
Webhook verification (Commune → your app)
Commune signs outbound webhooks using your inbox webhook secret. Verify the
signature before processing the request.`ts
import { createWebhookHandler, verifyCommuneWebhook } from "commune-ai";const handler = createWebhookHandler({
verify: ({ rawBody, headers }) => {
const signature = headers["x-commune-signature"];
const timestamp = headers["x-commune-timestamp"];
if (!signature || !timestamp) return false;
return verifyCommuneWebhook({
rawBody,
timestamp,
signature,
secret: process.env.COMMUNE_WEBHOOK_SECRET!,
});
},
onEvent: async (message) => {
// handle verified message
},
});
`---
Full example (single file)
A complete copy‑paste example that:
- receives webhook
- handles incoming attachments
- sends email with attachments
- replies by email`ts
import express from "express";
import { CommuneClient, createWebhookHandler } from "commune-ai";
import fs from "fs";const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
const handler = createWebhookHandler({
onEvent: async (message, context) => {
// 1) Handle incoming attachments
const { attachments } = context.payload;
if (attachments && attachments.length > 0) {
console.log(
Received ${attachments.length} attachments);
for (const att of attachments) {
// Get download URL
const { url, filename } = await client.attachments.get(att.attachment_id, { url: true });
console.log(Attachment: ${filename} - ${url});
// Download if needed
// const response = await fetch(url);
// const buffer = await response.arrayBuffer();
}
} // 2) Email reply with attachment (same thread)
const sender = message.participants.find(p => p.role === "sender")?.identity;
if (!sender) return;
// Upload attachment for sending
const fileBuffer = fs.readFileSync('receipt.pdf');
const base64Content = fileBuffer.toString('base64');
const { attachment_id } = await client.attachments.upload(
base64Content,
'receipt.pdf',
'application/pdf'
);
await client.messages.send({
channel: "email",
to: sender,
subject: "Your receipt",
text: "Thanks! Here's your receipt.",
attachments: [attachment_id],
conversation_id: message.conversation_id,
domainId: context.payload.domainId,
inboxId: context.payload.inboxId,
});
},
});
const app = express();
app.post("/commune/webhook", express.raw({ type: "/" }), handler);
app.listen(3000, () => console.log("listening on 3000"));
``