Email and the unified API
Understand why Samva exposes both an email send facade and a unified messages API, and how each fits your integration.
Samva gives you two ways to send email: a small, email-shaped email.send() facade and a
broader messages.send() API that maps directly onto POST /v1/messages. They are not competing
products or different transports; they are two views of the same send pipeline. This page explains
why both exist, how they relate, and what that design buys you over time.
Two shapes, one pipeline
Underneath, every send Samva performs is a message: a piece of content, a recipient, and a channel
that carries it. The unified API names those parts explicitly. When you call messages.send(), you
declare the channel, list your recipients in to, and nest the channel-specific content under a
matching key:
await samva.messages.send({
to: [{ contactId: "contact_123" }],
cc: [{ email: "grace@example.com" }],
channel: "email",
email: {
subject: "Welcome to Samva",
html: "<h1>Welcome!</h1><p>Thanks for joining.</p>",
replyTo: "support@example.com",
},
});The email.send() facade is the same call wearing email-shaped clothes. It assumes the channel is
email, flattens the email content up to the top level of the input, and accepts a single recipient
or an array. The result is a call that reads like sending an email rather than configuring a generic
message:
const result = await samva.email.send({
to: "ada@example.com",
cc: "grace@example.com",
replyTo: "support@example.com",
subject: "Welcome to Samva",
html: "<h1>Welcome!</h1>",
});There is no separate email.send HTTP endpoint. The facade is an SDK-only ergonomic wrapper: it
builds the unified body shape for you and posts it to POST /v1/messages. From the server's point of
view, both snippets above are the same request to the same endpoint. The difference lives entirely in
the SDK, in service of the developer reading the code.
Why a facade at all
Most email you send is straightforward. You have an address, a subject, and some HTML, and you want
the call site to say exactly that. Forcing every send to spell out channel: "email" and nest content
under an email: key adds ceremony to the common case without adding meaning; the channel is obvious
when you reached for email.send().
The facade optimizes for that common case. Flattening the email fields directly into the SDK input keeps the simple path short and removes a layer of nesting you would otherwise repeat on every send. It is the canonical way to send a one-off transactional email, and it is what the send an email guide walks through.
That convenience is deliberately narrow. The facade does not hide the unified API or replace it; it sits on top of it. When your needs outgrow "one address, one subject, one body," you drop down to the API the facade was built on, and nothing about the underlying send changes.
When to reach for the unified API
Reach for messages.send() (or POST /v1/messages directly) when the send is no longer a single
self-contained email:
- Addressing existing contacts. The unified
toarray accepts{ contactId }, so you can send to a contact you already store in Samva without re-supplying their email address. - Multiple recipients. When you are addressing a list rather than one person, the array form of
tois the natural fit, withccandbccavailable when you need mail-style copy semantics. - Threading a reply. When a send belongs to an existing conversation, the unified body is where you express that relationship so the message lands in the right thread.
The dividing line is conceptual, not performance- or feature-based: the facade is for "send this email
to this address," and the unified API is for everything that needs to talk about messages, recipients,
contacts, and conversations as first-class things. Because the facade compiles down to the same
request, you can start with email.send() and move to messages.send() later without rethinking how
your email is delivered.
REST is always the unified shape
The split between a facade and a unified API is an SDK convenience. It does not exist over the wire.
Every REST integration, whether or not you use the TypeScript SDK, sends to POST /v1/messages
using the unified body: a channel, a to array, and channel content nested under its key. If you
are calling the API directly, there is one shape to learn, and the SDK's email.send() is simply
producing that shape on your behalf.
This is why the REST reference and the TypeScript SDK reference can describe the same capability from two angles without contradiction: one documents the unified endpoint, the other documents the ergonomics layered over it.
Why the shape is a channel discriminator
The word "unified" in the unified API describes the send model, not just email. Modeling a send as a
channel plus channel-specific content is what lets POST /v1/messages address more than one kind of
message through a single endpoint. channel: "email" names the channel explicitly, and the email: {...}
block carries the subject, HTML, and text; that same shape, a channel value paired with its own content
block, is what any other channel addresses through the same to array and the same endpoint.
Crucially, that generality does not touch the email path. The email.send() facade is pinned to email by
definition, so its behavior, its fields, and what it sends never change. Code written against
email.send() keeps working unchanged no matter what else the unified shape carries. The unified API
absorbs the general case; the email facade stays exactly as small as it is today.
This is the tradeoff the two-API design resolves. A single generic API would make every email verbose. A single email-only API would have no room for a channel discriminator at all. By offering the facade for the common case and the unified API as its foundation, Samva keeps the simple thing simple while keeping the underlying model general.
Related
How Samva works
Understand how a send flows from your code to the inbox, the core objects that model email, and the single-API philosophy behind Samva.
Organizations and tenancy
How an API key binds to a Samva organization, why every record is tenant-scoped, and how teams and roles share that boundary.