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 samva

1. 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.email subdomain, such as support@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.
create-mailbox.ts
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.

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: if Samva retries the delivery, the retry returns the original reply instead of sending a second one.

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

Related documentation

On this page