Give an agent an inbox
Create a mailbox, give an agent its own key, receive a message by webhook, and reply in the same thread.
A mailbox is an email address your agent or app can read and send from. This tutorial creates one, gives an agent a key that reaches only that mailbox, and has the agent answer the first message it receives. When you finish, mail sent to the address reaches your server as a signed webhook, and your reply arrives in the sender's inbox in the same thread.
You need the TypeScript SDK, an API key with full access for setup, and a public HTTPS URL for the webhook. See Authentication for keys.
npm install samva1. Create the mailbox
A mailbox has a slug, a displayName, and at least one address. The address is either:
- a hosted address under your organization's
samva.emailsubdomain, such assupport@acme.samva.email, which needs no DNS setup; or - an address on one of your own domains. The domain must be verified and have receiving enabled.
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
const mailbox = await samva.mailboxes.create({
slug: "support",
displayName: "Support",
addresses: [{ address: "support@acme.samva.email" }],
});
console.log(mailbox.id, mailbox.primaryAddress);curl -X POST https://api.samva.dev/v1/mailboxes \
-H "X-API-Key: $SAMVA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"slug": "support",
"displayName": "Support",
"addresses": [{ "address": "support@acme.samva.email" }]
}'The response is 201 with the new mailbox: its id, primaryAddress, addresses, and status.
The caller receives an admin grant on it. The mailbox receives mail from now on; mail that arrived before it existed is not added. Creating a mailbox is
refused with 402 when the organization is at its plan's allowance; see pricing.
2. Give the agent its own key
A mailbox-scoped key is a restricted API key with mailboxes access. It acts as itself, named by the
key's name, and reaches only the mailboxes you list. permissions chooses what it may do: read,
update, send, and quarantine.read. Agents and keys explains
each.
const key = await samva.apiKeys.create({
name: "Support agent",
access: {
mode: "restricted",
resources: {},
mailboxes: {
permissions: ["read", "update", "send"],
mailboxIds: [mailbox.id],
sendMode: "send",
},
},
});
// Store this now. Samva does not return it again.
const agentKey = key.key;curl -X POST https://api.samva.dev/v1/api-keys \
-H "X-API-Key: $SAMVA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Support agent",
"access": {
"mode": "restricted",
"resources": {},
"mailboxes": {
"permissions": ["read", "update", "send"],
"mailboxIds": ["mbx_..."],
"sendMode": "send"
}
}
}'sendMode defaults to send. Set it to approval to hold each of the agent's sends until a person
approves it; see Approve sends.
3. Subscribe a webhook to the mailbox
Create a webhook endpoint for mailbox.message.received and filter it to the mailbox with
mailboxIds. Signing, delivery, and retries work as they do for every other webhook; see
Receive webhooks.
const endpoint = await samva.webhooks.create({
name: "Support agent inbox",
url: "https://your-app.com/webhooks/samva",
eventTypes: ["mailbox.message.received"],
mailboxIds: [mailbox.id],
});
// Store this now. Samva does not return it again.
const signingSecret = endpoint.secret;curl -X POST https://api.samva.dev/v1/webhooks \
-H "X-API-Key: $SAMVA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Support agent inbox",
"url": "https://your-app.com/webhooks/samva",
"eventTypes": ["mailbox.message.received"],
"mailboxIds": ["mbx_..."]
}'A server without a public URL can read the same events over a WebSocket event stream instead.
4. Reply to the message
Your endpoint verifies the request, then replies with the agent's key. The webhook event ID is a
stable Idempotency-Key: if Samva retries the delivery, the retry returns the original reply instead
of sending a second one.
import { createClient } from "samva";
import { verifyRequest } from "samva/webhooks";
const agent = createClient({ apiKey: process.env.SAMVA_AGENT_KEY! });
export async function POST(request: Request) {
const { id, event } = await verifyRequest(request, process.env.SAMVA_WEBHOOK_SECRET!);
if (event.type !== "mailbox.message.received") return new Response(null, { status: 204 });
const { mailboxId, message } = event.data as {
mailboxId: string;
message: { id: string };
};
const receipt = await agent.mailboxes.reply({
id: mailboxId,
messageId: message.id,
text: "Thanks, we got your message.",
"idempotency-key": id,
});
console.log(receipt.status, receipt.threadId);
return new Response(null, { status: 204 });
}The same reply as a REST call:
curl -X POST https://api.samva.dev/v1/mailboxes/$MAILBOX_ID/messages/$MESSAGE_ID/reply \
-H "X-API-Key: $SAMVA_AGENT_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: whevt_..." \
-d '{ "text": "Thanks, we got your message." }'The server sets the recipients, the subject, and the threading headers. The receipt's status is
sent, or pending_approval when the agent's sends wait for a person, and skippedRecipients names
any recipient Samva did not send to and why. Reusing a key with a different body is a 409.
Now send an email to support@acme.samva.email from any address. Your endpoint receives the event,
and the sender gets the reply in the same conversation.
Next steps
Agents and keys
People, keys, and namespaces, and what each can do.
Approve sends
Hold an agent's sends until a person approves them.
Quarantine
Suspicious mail is held until a person decides.
Events
Webhooks and the event stream.
Connect over MCP
Let an MCP client read and reply.
Mailbox CLI
Read, send, and tail events from a terminal.