Skip to Content

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 is accepted in the same thread, subject to sending limits and delivery checks.

This tutorial creates a shared mailbox. To give a person their own private address, follow Give a person a mailbox.

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 samva @samva/webhooks

1. Create the mailbox

A mailbox has a name and one email endpoint, and the mailbox sends from that endpoint's address. The address is either:

  • a hosted address under your organization's samva.email subdomain, such as support@acme.samva.email, which needs no DNS setup. The dashboard's Create mailbox dialog and samva org current show your organization's hosted domain. API and SDK callers read organization.hostedMailDomain from GET /v1/whoami; do not guess it from the slug; or
  • an address on one of your own domains. The domain must be verified, active, and have catch-all receiving enabled. You can enable receiving before creating any mailbox, webhook endpoint, or forwarding rule.

Samva reads which of the two it is from the address. The mailbox's slug is its unique handle; it defaults to the address's local part, so support@acme.samva.email creates the slug support. Pass slug yourself when two addresses would give the same one, such as support@acme.samva.email and support@acme-corp.example.com.

create-mailbox.ts
import { createClient } from "samva";

const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });

const mailbox = await samva.mailboxes.create({
  endpoint: { channel: "email", address: "support@acme.samva.email" },
  name: "Support",
});

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 '{
    "endpoint": { "channel": "email", "address": "support@acme.samva.email" },
    "name": "Support"
  }'

A mailbox starts with one endpoint, which is its default; add more later. The response is 201 with the new mailbox: its id, name, slug, primaryAddress, endpoints, and status. A hosted address receives mail at once. Sending can wait while your hosted address becomes ready; follow the action receipt as Approve sends describes. 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.

create-agent-key.ts
const key = await samva.apiKeys.create({
  name: "Support agent",
  access: {
    mode: "restricted",
    resources: {},
    mailboxes: {
      permissions: ["read", "update", "send"],
      mailboxIds: [mailbox.id],
    },
  },
});

// 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_..."]
      }
    }
  }'

Keys default to allowing sends. Set a decision policy to hold an agent's sends until a person approves them; 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.

create-webhook.ts
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 on your server. GET /v1/webhooks/{id}/secret reads 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: a retry within the idempotency window returns the original reply instead of sending a second one. See Send receipts for the retry boundary.

app/webhooks/samva/route.ts
import { verifyRequest } from "@samva/webhooks";
import { createClient } from "samva";

const samva = 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 samva.mailboxes.reply(
    {
      id: mailboxId,
      messageId: message.id,
      text: "Thanks, we got your message.",
    },
    { idempotencyKey: id },
  );

  console.log(receipt.state, receipt.actionId);
  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, subject, and threading headers. The 202 receipt identifies the action and its state, such as queued or approvalRequired. Preserve actionId and follow it with mailboxes.getAction; acceptance makes messageId available. Approve sends explains every state, the sending deadline, and cancellation. Reusing a key with a different body is a 409; after a lost response, keep the original key and body.

Now send an email to support@acme.samva.email from any address. Your endpoint receives the event, and the reply stays in the same conversation. Check the receipt before assuming it has gone out.

Next steps

To build the agent with a framework, follow the guide for the Claude Agent SDK, the Vercel AI SDK, Eve, the OpenAI Agents SDK, or LangGraph.

Run the example

The mailbox-agent example is a runnable project that answers mail from an agent's mailbox. The mailboxes cookbook goes deeper on threads, approval, and the event stream.

Related documentation

On this page