Build mailbox extensions
Write code that reacts to mailbox events, publish it, and install it with its own permissions on your webhook or on Samva.
An extension is code you publish once and install into an organization. It receives mailbox events and acts through the mailbox API, for example to acknowledge every new message or to label threads. You choose where it runs:
| Runtime | Where the code runs | How events reach it |
|---|---|---|
webhook | Your server, at an HTTPS URL you give at install time. | A signed webhook. You call the API with a key revealed once. |
hosted | Samva, from a bundle you publish. No server to run. | Samva runs your handler for each event. |
Write an extension
Install the authoring package @samva/mailbox, or scaffold a project with the CLI:
samva extensions init ./acknowledge --name acknowledge --runtime hosted--runtime is hosted (the default) or webhook. The directory must be empty. The project contains
src/extension.ts, and a webhook project adds src/server.ts.
import { defineExtension } from "@samva/mailbox";
export default defineExtension({
manifest: {
name: "acknowledge",
displayName: "Acknowledge new mail",
version: "1.0.0",
runtime: "hosted",
events: ["mailbox.message.received"],
permissions: ["read", "send"],
},
on: {
"mailbox.message.received": async (event, { samva, log }) => {
const reply = await samva.messages.reply(event.data.mailboxId, event.data.message.id, {
text: "Thanks, we got it.",
});
// "approvalRequired" waits for a person; "queued" retries until its `executeUntil` deadline.
log(`Reply ${reply.state}`, event.data.message.id);
},
},
});The manifest
| Field | Rules |
|---|---|
name | 3 to 64 lowercase letters, digits, and hyphens. It identifies the extension in your organization. |
displayName | 1 to 80 characters. |
description | Optional, up to 500 characters. |
version | Semver, such as 1.0.0. A published version is immutable. |
runtime | hosted or webhook. |
events | One or more of the mailbox event types, each listed once. |
permissions | The mailbox permissions it may use: read, update, send, quarantine.read. read is required. |
Every key in on must appear in events. An event with no handler is acknowledged and skipped.
What a handler receives
A handler gets the event (id, type, timestamp, data) and a context with:
samva, a mailbox client withmessages.get,reply,replyAll,forward, andsend, andthreads.get,list,messages, andupdate.log(...), which records a line in the run's log.attempt, the delivery attempt number.
Sends made through samva carry an Idempotency-Key of the form <event id>:<method>:<n>, so a
handler that runs again for the same event does not send twice. Other side effects need their own
deduplication. Throwing from a handler fails the event, which Samva then retries.
If Samva temporarily cannot check the installation’s access, the event waits without starting the handler or consuming a handler attempt. A failed run whose mailbox API calls return a server error or cannot reach Samva also waits without exhausting its handler attempts. An invalid or revoked credential stops the installation and removes its waiting events. Outage retries slow down with a bounded delay, independently of handler attempts. If an event’s credential expires while it waits, Samva renews it without requiring another event. Renewal preserves its original permission, scope, and approval limits and applies any current restrictions.
Test locally
@samva/mailbox/test runs a handler without a network.
import { createTestClient, mailboxEvent, runExtension } from "@samva/mailbox/test";
import extension from "./src/extension";
const client = createTestClient();
const result = await runExtension(extension, mailboxEvent("mailbox.message.received"), { client });
console.log(result.ok, result.logs, result.calls);To run your handlers against live events, use samva extensions dev. It tails the
event stream and runs the extension for each event whose type
is in the manifest. It calls the API with your own credential, so actions are attributed to you, not
to an installation.
samva extensions dev . --mailbox mbx_...Publish a version
Publishing and installing need a person signed in with samva login who can manage mailboxes. The
CLI builds the project before it publishes. A hosted extension builds to one self-contained ES module, which must stay within
the hosted limits.
samva extensions build . --out dist/extension.js
samva extensions publish .publish creates the extension on its first version and prints the version it recorded. Publishing
the same version with the same content again returns it; different content under the same version
is a 409, so bump version.
List what you published with samva extensions list, an extension's versions with
samva extensions versions <name>, and one version with its manifest with
samva extensions version <name> <version>.
Install it
An installation applies a published version to your mailboxes. It is a principal of its own: it acts on the installer's grants, narrowed by what you give it here, and never exceeds them.
samva extensions install acknowledge \
--mailbox mbx_... \
--permission read --permission send \
--send-mode approval| Flag | Meaning |
|---|---|
--version | A published version. The latest when omitted. |
--mailbox | Repeat to name mailboxes. Omit it to reach every mailbox the installer reaches. |
--namespace | Confine the installation to one namespace. |
--permission | Repeat to narrow the manifest's permissions. It can only remove permissions, never add them. |
--send-mode | send or approval. With approval, every send waits for a person. |
--webhook-url | For a webhook extension, the HTTPS URL that receives events. |
An installation receives events only while it is active and its principal can read the event's
mailbox right now. Quarantine events also need quarantine.read. An installation can never approve
a send, release or discard held mail, or change grants and keys. If the installer loses the mailbox,
the installation stops receiving its events.
List installations with samva extensions installations, read one with
samva extensions installation <id>, and manage it with disable, enable, and uninstall.
Disabling stops the installation at once and refuses its credential on every surface; events
published while it was disabled are not delivered after you enable it. Uninstalling deletes the
installation and its key, and the actions it took keep their attribution. Disabling or uninstalling
also cancels definitely-unsent queued sends proposed by that installation, including human-approved
sends. Accepted emails cannot be withdrawn. A held send records decision.source: "installation"
when its installation requires approval; see Approve sends for receipts
and progress.
When you re-enable a hosted installation, it can receive new events. A delayed revocation from an earlier disable cannot remove those events or invalidate their credentials.
Upgrade or roll back an installation
A person who can manage mailboxes can move an installation to another published version of the
same extension. Sign in with samva login; an API key or an extension credential cannot upgrade
an installation. Publishing a version does not change existing installations.
Preview the change first:
samva extensions upgrade exti_... --version 2.0.0 --dry-run --json--version accepts a published semver or version id. Omitting it selects the extension's latest
published version. The preview returns fromVersion, toVersion, and permissionDiff with
added and removed grant strings. It returns the installation's current state, sets dryRun to
true and versionChange to null, and changes nothing. A preview needs no permission
acknowledgment.
Review every entry in permissionDiff.added. To apply the change, repeat --accept-permissions
once for each exact string. For example, if the preview adds permission:send and
event:mailbox.message.sent:
samva extensions upgrade exti_... --version 2.0.0 \
--accept-permissions permission:send \
--accept-permissions event:mailbox.message.sent \
--yesThe acknowledgment list must match the added grants exactly. Omit --accept-permissions when
there are no additions. --yes confirms the version change in automation; it does not acknowledge
any added grant. In a terminal, omitting --yes asks for confirmation.
The installation keeps its id, credentials, webhook binding, mailbox and namespace scope, send mode, attribution, and enabled or disabled status. Removed permissions take effect immediately; removed event subscriptions stop new deliveries. For hosted extensions, new events use the selected version; events already waiting or running use the version they started with. Their credentials never gain the new version's added authority, and the installation's current narrower scope still applies.
To roll back, choose the previous published version with --version and follow the same preview
and acknowledgment steps:
samva extensions upgrade exti_... --version 1.0.0 --dry-run --json
samva extensions upgrade exti_... --version 1.0.0 --yesThe second command succeeds only if the rollback adds no grants. Otherwise, pass each added grant from its preview explicitly.
Upgrade through the SDK or API
The SDK exposes extensionInstallations.upgrade, backed by
POST /v1/extension-installations/{id}/upgrade. Both require a person credential. The request
takes optional version, acceptPermissions (an array of exact added grant strings), and
dryRun (a boolean). Use the same target for the preview and the applied change:
const preview = await samva.extensionInstallations.upgrade({
id: "exti_...",
version: "2.0.0",
dryRun: true,
});
console.log(preview.fromVersion, preview.toVersion, preview.permissionDiff);
// After reviewing the added grants, name each one explicitly.
const result = await samva.extensionInstallations.upgrade({
id: "exti_...",
version: "2.0.0",
acceptPermissions: ["permission:send", "event:mailbox.message.sent"],
});
console.log(result.installation, result.versionChange);The response contains installation, fromVersion and toVersion (each with id and version),
permissionDiff, dryRun, and versionChange. An applied version change returns a
versionChange record with id, organizationId, installationId, extensionId, fromVersion,
toVersion, permissionDiff, changedBy, and changedAt. Selecting the current version with no
grant changes returns versionChange: null.
Run on your server
A webhook installation returns an API key and a webhook secret once, when you install it. Store both.
createWebhookHandler verifies each request's signature, runs your handler, and answers 200 on
success or 500 so Samva retries.
import { createWebhookHandler } from "@samva/mailbox/webhook";
import extension from "./extension";
Bun.serve({
fetch: createWebhookHandler(extension, {
apiKey: process.env.SAMVA_API_KEY!,
webhookSecret: process.env.SAMVA_WEBHOOK_SECRET!,
}),
});Delivery, signing, and retries follow the webhook event catalog.
Run on Samva
A hosted extension needs no server and no credential. Samva runs your bundle for each event. The
bundle has no network access: its only way out is samva, the mailbox client, which can do exactly
what the installation's principal can do through the mailbox API.
List an installation's recent runs, then read one to see the lines your handler logged:
samva extensions runs exti_...
samva extensions run exti_... <run-id>The list shows each run's event, attempt, outcome, and duration; the run itself adds its logs. A run's outcome is succeeded, failed, timed_out, budget_exhausted, or revoked.
Hosted limits
| Limit | Value |
|---|---|
| Bundle size | 1 MiB |
| CPU time per run | 1 second |
| Mailbox API calls per run | 50 |
| Wall time per run | 30 seconds |
| Runs per installation per hour, retries included | 600 |
| Attempts per event | 5 |
| Log lines kept per run | 50 |
| Characters kept per log line | 500 |
| Runs kept in the run log | 100 |
An event that arrives after an installation has used its hourly runs is recorded as
budget_exhausted and not run.
Manage extensions over the API
The CLI commands call the API, and the SDK exposes the same operations as the extensions and
extensionInstallations namespaces.
| To | Call |
|---|---|
| Publish a version | POST /v1/extensions; it answers with the extension and the version. |
| List extensions | GET /v1/extensions, then GET /v1/extensions/{id}. |
| List versions | GET /v1/extensions/{id}/versions, one summary per version. |
| Read one version | GET /v1/extensions/{id}/versions/{versionId}, with the manifest. |
| Install a version | POST /v1/extension-installations; the credentials of a webhook extension come back once. |
| Upgrade or roll back | POST /v1/extension-installations/{id}/upgrade; preview and acknowledge added grants. |
| List or read installations | GET /v1/extension-installations and GET /v1/extension-installations/{id}. |
| Disable, enable, or uninstall | POST /v1/extension-installations/{id}/disable, .../enable, and DELETE on the installation. |
| List runs or read one | GET /v1/extension-installations/{id}/runs lists runs without logs; .../runs/{runId} adds them. |
The API reference lists the fields of each.