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.

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

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 shows your organization's hosted domain; or
  • an address on one of your own domains. The domain must be verified and have receiving enabled.

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],
      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.

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 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: 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 { 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.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

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