Namespaces
Give each of your customers a namespace with its own domains, contacts, and templates, a key that reaches only that namespace, and usage readback to bill from.
A namespace is a sub-tenant inside your organization. If you run a platform that sends email on behalf of your own customers, create one namespace per customer and one API key per namespace. That customer's key reaches their domains, senders, contacts, and templates and nothing else, and you read each customer's message counts to charge them. Samva bills your organization; the per-customer split is yours to read and bill.
Set up a customer
This walkthrough creates a namespace, issues it a scoped key, and sends from it with that key. Run the first two steps with a key that can manage the organization.
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
// 1. One namespace per customer. `metadata` is yours; Samva never reads it.
const namespace = await samva.namespaces.create({
name: "Acme Corp",
metadata: { customerId: "cus_123" },
});
// 2. A key that reaches only that namespace.
const created = await samva.apiKeys.create({
name: "Acme Corp",
access: {
mode: "restricted",
namespaceId: namespace.id,
resources: { domains: "write", contacts: "write", templates: "write", email: "write" },
},
});Keep SAMVA_API_KEY as your organization key. Store created.key as
SAMVA_CUSTOMER_API_KEY and namespace.id as SAMVA_NAMESPACE_ID in your server environment.
Each following example is a separate module.
Use the customer's key for their requests. Resources created with it land in the namespace:
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_CUSTOMER_API_KEY! });
await samva.email.addDomain({ domain: "mail.acme.example" });
await samva.contacts.create({
name: "Ada Lovelace",
identities: [{ type: "email", identifier: "ada@example.com" }],
});Publish the DNS records the domain returns and wait for it to verify, as in Verify your sending domain. Then send with the customer's key:
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_CUSTOMER_API_KEY! });
const message = await samva.email.send({
from: { email: "hello@mail.acme.example" },
to: "ada@example.com",
subject: "Welcome to Acme",
html: "<p>Thanks for signing up.</p>",
});Read the customer's usage with your own key:
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
const usage = await samva.namespaces.getUsage({
id: process.env.SAMVA_NAMESPACE_ID!,
after: "2026-10-01T00:00:00Z",
before: "2026-11-01T00:00:00Z",
});
console.log(usage.emailCount, usage.totalMessages);curl "https://api.samva.dev/v1/namespaces/$NAMESPACE_ID/usage?after=2026-10-01T00:00:00Z&before=2026-11-01T00:00:00Z" \
-H "X-API-Key: $SAMVA_API_KEY"The same steps are available from the CLI (samva namespaces create, then --namespace on the
domains, senders, contacts, and templates commands), from the hosted MCP server as the namespaces_*
tools, and in the dashboard under Namespaces, where the API key form has a namespace picker.
To let your customer verify domains and read activity without creating a Samva account, give them a customer portal.
What belongs to a namespace
Domains, senders, contacts, and templates each belong to one namespace or to the organization
itself. A resource outside every namespace is at organization level: its namespaceId is null.
- Create a resource inside a namespace by passing
namespaceIdon its create call. A key confined to a namespace always creates inside it. - List endpoints take
?namespaceId=to filter to one namespace, including mailboxes, webhook endpoints, agents, scheduled messages, conversations, knowledge sources, and API keys. The CLI uses--namespace. A confined key lists only its own resources on endpoints it is permitted to use. A mailbox filter also preserves the caller's mailbox access. A filter can only narrow what the credential reaches; it never grants access to another namespace. - A sender on a domain belongs to the domain's namespace.
- Mailboxes, API keys, and webhook endpoints keep the namespace they were created with and do not move.
- Defaults are per scope. Setting a default sender or domain clears only the other default in the same namespace, or at organization level.
The same email address can be a contact in several namespaces and at organization level, once in each. A contact in one namespace is a different contact from the one with the same address in another.
Confine a key to a namespace
A key created with access.namespaceId reaches only that namespace's resources. It never reaches
organization-level resources other than shared templates, shared domains,
and read-only brands.
A resource in another namespace or at organization level answers 404, the same as one that does
not exist.
Such a key may hold grants on namespace-aware resources, including agents, analytics, conversations,
and the channels it sends through. Scheduled messages use the messages grant, and knowledge
sources use agents. Organization brands are readable for template rendering, but a namespace key
cannot create, update, or delete them.
Endpoints that cannot confine to a namespace answer 403 for it, including contact groups and
lists, custom fields, campaigns, webhook management, and organization settings. Manage those with a
key that is not confined, and use a webhook endpoint filtered to the namespace to receive its events
(Webhooks).
People
Invite a customer's staff with a namespace on the members invitation. Their member, admin, or Mail user role is limited to that namespace's resources. The dashboard shows Organization · Namespace in the switcher and hides organization-wide administration. Mail lists only reachable mailboxes in that namespace. Changing a route, request header, or OAuth selection cannot widen their membership. Keys they create stay inside the same namespace.
Their personal mailbox belongs to the namespace and uses one of its domains or a shared organization domain. Add a usable domain to the namespace or share an organization domain before inviting: an invitation without either is refused with an add-domain remedy. The mailbox sends with the namespace's sending health and suppression rules, and the namespace's keys and OAuth connections reach it with the required mailbox permissions. Unrestricted members keep organization-level personal mailboxes. Restricted members prevent deleting the namespace.
When connecting an OAuth or hosted MCP client, choose the organization, then select a namespace on the consent screen. The connection acts as you within that namespace, with your role's grants limited to the resources a namespace key can use. For an unrestricted member, leaving Whole organization selected grants their full role across the organization. A restricted member's connection stays inside their membership namespace.
A namespace connection lists only its namespace's resources. A resource in another namespace or at
organization level answers 404, except shared templates, shared domains, and read-only brands. Organization-wide
endpoints, including webhook management and organization settings, answer 403. Mailbox access
also requires your existing mailbox grants; choosing a namespace never grants access to a mailbox.
The connection keeps its namespace when its token refreshes. Choosing a namespace for a new connection does not change the dashboard's active organization or another connection's authority. Removing organization membership stops access.
Sends
Everything a send uses must sit in one namespace: the sender or domain, the template, and any contacts you name by id. Shared templates and senders on shared domains are exceptions: the message sends in whichever namespace the rest of the send uses.
- A confined key sends in its own namespace. Naming anything outside it is a
404. - A key that is not confined may combine organization-level resources with one namespace's, never
with two namespaces. Mixing two is a
422. - Without
from, the send uses the namespace's default sender. A key that is not confined falls back to the organization's default sender when the namespace has none. A confined key with no default in its namespace must passfrom, or the send fails withFromAddressRequiredError. - Recipients you name by address become contacts in the send's namespace. A recipient you name by contact id must already be in it.
Scheduled messages, analytics, and conversations
A scheduled message keeps the namespace chosen when it is created. Pass namespaceId on the
schedule's create body, or --namespace to samva scheduled-messages create. A namespace key
always schedules in its own namespace. When the schedule sends later, it uses that saved namespace;
a retry or resume does not choose a different one. Resources used by the send must remain reachable
in that namespace, with the shared-template and shared-domain exceptions.
Analytics and conversation reads are confined to the key's namespace, including conversation
messages and analytics exports. With a key that is not confined, pass namespaceId to narrow an
analytics report or conversation list. The CLI commands are samva analytics stats,
samva analytics delivery-metrics, and samva conversations list, each with --namespace.
samva conversations create --namespace places the new conversation in that namespace.
The hosted MCP tools take namespaceId on analytics_get_stats, conversations_list,
conversations_create, scheduled_messages_list, and scheduled_messages_schedule_email.
Agent knowledge
A knowledge source belongs to one namespace or to organization level. An agent can attach and search only sources in its own namespace. Several agents in the same namespace can use one source; knowledge sources cannot be shared across namespaces. Organization-level sources are available only to organization-level agents.
Pass namespaceId when adding a website or file, or use samva knowledge add-website and
samva knowledge add-file with --namespace. samva knowledge list --namespace filters sources.
The hosted MCP tools knowledge_list, knowledge_add_website, and knowledge_add_file take
namespaceId; a namespace key creates and reads only in its own namespace.
Isolation
Each namespace has its own email sending health. A sending pause in one namespace holds that namespace's email without pausing another namespace or organization-level email. When sending resumes, held email can continue within its send window.
Subscribe to email.sending.paused and email.sending.resumed
to track these changes. Their namespaceId identifies the affected namespace; null means
organization-level email. A webhook endpoint filtered to a namespace receives only that namespace's
changes.
Namespaces share the organization's plan, billing, and organization-wide sending restrictions. Suppressions protect their own scope. A bounce, complaint, consent opt-out, or import restriction recorded inside a namespace suppresses the address only there. An organization-level suppression applies across every namespace as well as organization-level sending. An address can therefore be suppressed in one namespace and reachable in another, unless an organization-level suppression also applies.
Use samva.email.listSuppressions({ namespaceId }) or samva suppressions list --namespace to
read exactly one namespace's entries with an organization key. Without a filter, an organization key
lists all scopes. samva.email.countSuppressions({ namespaceId }) counts active entries in that
scope. Each list entry returns its namespaceId; null means organization-wide protection.
A namespace key lists, counts, and removes only its own entries. It cannot inspect or remove an
organization-level suppression, even when that suppression prevents its send.
Review an entry's reason before removing it by id with samva.email.removeSuppression({ id }) or
samva suppressions remove <id> --yes. Removal affects only that entry: another applicable
suppression can still prevent delivery. Hosted MCP offers suppressions_list with an optional
namespaceId filter and suppressions_remove by id.
Share templates
To give every customer the same templates, keep them at organization level and share them. A
confined key reads, lists, and sends a shared template, but cannot change, publish, delete, or
move it; those calls answer 403. Edit shared templates with a key that is not confined.
# Share an organization-level template with every namespace, with a key that is not confined.
curl -X PATCH https://api.samva.dev/v1/templates/$TEMPLATE_ID \
-H "X-API-Key: $SAMVA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "data": { "shared": true } }'import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_CUSTOMER_API_KEY! });
// The customer's key sends it in the customer's namespace.
await samva.email.send({ to: "ada@example.com", templateSlug: "order-receipt" });POST /v1/templates also takes "shared": true to create a template shared from the start.
From the CLI:
samva templates update $TEMPLATE_ID --data '{}' --shared true
samva templates create --data '{"name":"Receipt"}' --idempotency-key receipt-v1 --shared
samva templates list --shared true- Sharing belongs to the template's project: every template in the project is shared with it.
- Only an organization-level template is shared. A shared template cannot move into a namespace; stop sharing it first.
- A send from a confined key runs in its own namespace: its sender, its recipients, and its usage.
The template's
fromdefault names an organization-level sender, so the send uses the namespace's default sender instead, or thefromyou pass. - Templates carry
shared.GET /v1/templates?shared=truelists only shared templates, and?namespaceId=lists what that namespace uses: its own templates and the shared ones.
A shared WhatsApp template sends on the business account the send uses: the namespace's number for
a confined key. Meta approves a template per business account, so submit the shared template on each
account its customers send from, with a key that is not confined. A send on an account without an
approval answers 409 naming the account and the registration call that fixes it.
Move resources between namespaces
Assign an existing resource to a namespace, or return it to organization level, with a key that manages the organization:
curl -X POST https://api.samva.dev/v1/namespaces/$NAMESPACE_ID/resources \
-H "X-API-Key: $SAMVA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "type": "domain", "resourceId": "dom_..." }'
curl -X DELETE https://api.samva.dev/v1/namespaces/$NAMESPACE_ID/resources/domain/dom_... \
-H "X-API-Key: $SAMVA_API_KEY"Use samva namespaces assign and samva namespaces unassign for the same moves:
samva namespaces assign "$NAMESPACE_ID" --domain "$DOMAIN_ID"
samva namespaces unassign "$NAMESPACE_ID" --domain "$DOMAIN_ID"The hosted MCP tools are namespaces_assign_resource and namespaces_remove_resource.
In the dashboard, open a namespace and use the searchable resource picker to move a resource in,
or return it to organization level. Resource lists offer a namespace filter and show namespace
labels.
type is domain, sender, contact, or template. Some resources bring others with them:
- A domain brings its senders. A sender on a domain cannot move alone; move the domain. A shared domain stays at organization level; replace namespace defaults and unshare it before moving.
- A contact brings its addresses.
- A template brings every template in its project. A shared template stays at organization level.
A moved resource stops being a default. Keys confined to the old namespace stop reaching it at once. An address the target namespace already holds conflicts.
Move a WhatsApp number with type: "whatsappNumber", or --whatsapp-number in the CLI.
A confined key can select an unknown-recipient mailbox for a domain only when that active mailbox belongs to the same namespace and has an email address on the domain.
Usage
GET /v1/namespaces/{id}/usage counts the namespace's messages by channel in the window you pass
as after and before. It counts the same statuses as organization usage. Read it before you delete
the namespace: a deleted namespace has no usage to read.
Webhooks
A webhook endpoint created with namespaceId receives only that namespace's events: message,
mailbox, contact, domain, and email sending-health events. An endpoint without a filter receives
every event in the organization. Manage endpoints with a key that is not confined to a namespace.
Delete a namespace
A namespace that still holds anything cannot be deleted. The conflict names what remains and how many of each. Delete those resources, or return movable ones to organization level, then delete the empty namespace.
Once deletion starts, the namespace stops accepting new resources while its email tenant is removed. If deletion reports that cleanup is running, retry the same delete request after it finishes.
The endpoints are in the namespaces API reference.
Mailbox keys, grants, and mailbox webhooks work inside a namespace as described in Agents, people, and keys.