Start with Samva vs Resend if you are still choosing. It compares the published plans. Return here once the decision is made: this is the implementation path for one reversible flow, not another pricing comparison.
Resend sends email, accepts a batch of up to 100 messages, renders published templates, signs webhooks, verifies a domain you own, and receives mail on that domain. The facts below are from Resend's own docs, checked October 16, 2026, and linked at the end. Where Samva does not offer the same call, the table says so.
Samva is an email API. A reply threads into the conversation of the email it answers, and your
application gets that reply as a signed message.received event with the sender's address and the
conversation id. A later send stays on that thread only when you pass the conversation, or the
message it replies to. You change the email in the AI email editor or in a Git-backed template, and
you can drive the same API from the TypeScript SDK, the CLI, or hosted MCP.
Map the send call
resend.emails.send and POST https://api.resend.com/emails become samva.email.send or
POST https://api.samva.dev/v1/messages. The send an email
guide is the full request. This is the field map.
| Resend send field | Samva destination | What to decide |
|---|---|---|
from | from: { email, name }, optional | Resend requires from. If you omit it, Samva uses the template default, then the organization default sender, then your only verified sender. With several verified senders and no default, the send fails validation. |
to, cc, bcc | to, cc, bcc | A string, { email }, or { contactId }. Resend caps to at 50 addresses. |
subject, html, text | subject plus html or text | Inline content needs a subject and at least one body. A template send omits all three. |
react (Node.js SDK) | No react field | Send HTML, or publish a template and pass its slug. The send call does not accept a React component. |
reply_to | replyTo | An array of addresses, or null. |
headers | No field | The send payload has no custom email headers. See below. |
tags | metadata | A JSON object stored on the message and returned on message events. It is not an email tag, and it is not a conversation tag. |
scheduled_at | scheduledMessages.create | messages.send has no scheduling field. The scheduled message is a separate resource you can read or cancel. |
attachments | attachments | Media you have uploaded, or bounded base64 content. Resend's batch endpoint does not accept attachments; a single Resend send does. |
template.id, template.variables | templateSlug or templateId, plus templateData | Name one published template. Do not also send subject, html, or text. |
Idempotency-Key | Idempotency-Key | Resend keys expire after 24 hours and allow 256 characters. Pass your own idempotencyKey on the Samva call; the SDK otherwise generates a fresh key. The REST API states Samva's rules. |
topic_id | unsubscribeGroupId on the email, when you mean a preference | A Resend topic decides send or fail from the contact's opt-in. An unsubscribe group records which mail an address has declined. They are not the same object. |
The send API has no custom email headers. Resend's send reference includes a headers object.
Samva's email content does not. You cannot set List-Unsubscribe, X-Entity-Ref-ID, or any other
header of your own on email.send or POST /v1/messages. If the flow needs a header you supply,
keep it on Resend or drop the header before cutover.
Samva still writes headers a send needs. A reply that names the conversation or the message it
answers gets In-Reply-To and References from that message. A campaign adds one-click unsubscribe
headers for its unsubscribe group. Those are not a place to put your own headers.
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
const message = await samva.email.send(
{
from: { email: "receipts@example.com", name: "Acme" },
to: "ada@example.com",
replyTo: "support@example.com",
subject: "Your receipt",
html: "<p>Thanks for your order.</p>",
metadata: { orderId: "A-1042" },
},
{ idempotencyKey: "receipt:A-1042" },
);
console.log(message.id, message.status);
The REST call is the same product operation:
curl -X POST https://api.samva.dev/v1/messages \
-H "X-API-Key: $SAMVA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: receipt:A-1042" \
-d '{
"from": { "email": "receipts@example.com", "name": "Acme" },
"to": [{ "email": "ada@example.com" }],
"channel": "email",
"metadata": { "orderId": "A-1042" },
"email": {
"replyTo": ["support@example.com"],
"subject": "Your receipt",
"html": "<p>Thanks for your order.</p>"
}
}'
Persist message.id with the order. Acceptance is not delivery.
Replace a batch with one send per email
Resend's resend.batch.send and POST /emails/batch accept up to 100 emails in one request. Each
entry can differ. Attachments are not supported on that endpoint. The response lists ids in the
same order as the request.
Samva has no batch send endpoint. There is no POST /v1/messages body that takes an array of
different emails. Send each one, with an idempotency key derived from the business id, so a retry
returns the original message instead of sending a second copy:
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
const orders = [
{ id: "A-1042", email: "ada@example.com" },
{ id: "A-1043", email: "grace@example.com" },
];
for (const order of orders) {
const message = await samva.email.send(
{
to: order.email,
subject: `Receipt ${order.id}`,
html: `<p>Order ${order.id} is confirmed.</p>`,
},
{ idempotencyKey: `receipt:${order.id}` },
);
console.log(order.id, message.id);
}
A campaign is the other shape: one subject and body, many contacts, one run. Use it when Resend
today sends the same email to a list, including a broadcast. It is not a stand-in for a batch of
distinct messages. Send a campaign is that path.
Schedule an email is the path for scheduled_at: one
known email at a future instant, created with scheduledMessages.create, not a field on the send.
Map templates
Both products store templates, render them at send time, and separate a draft from the published
version a send can use. Resend's template docs describe a no-code editor, version history, variables,
and creating a template from HTML or React Email. You pass template.id (an id or alias) and
template.variables. from, subject, and reply_to on that request override the template
defaults.
Samva templates are versioned TSX in a Git-backed project. You publish a commit, and you can roll
back to an earlier publication. The AI email editor is one way to change the template. A send names
templateSlug or templateId and passes templateData. Omit subject, html, and text. To pin
one publication, or to follow new publications while the input shape stays the same, see
Choose which publication renders.
await samva.email.send(
{
to: "ada@example.com",
templateSlug: "order-confirmation",
templateData: { orderId: "A-1042", total: "$42.00" },
},
{ idempotencyKey: "receipt:A-1042" },
);
Keep sending inline HTML until the template is published and a test send matches. The first cutover does not require a template rewrite. Resend's version history and Samva's publications are both real. Move the content you need, then point the send at the slug.
Map webhooks and events
Keep /webhooks/resend and /webhooks/samva as separate routes while both providers can still
deliver. Resend's webhook introduction documents signature verification and replay. Samva signs
with the Standard Webhooks headers webhook-id, webhook-timestamp, and webhook-signature.
Pass the untouched request to verifyRequest from samva/webhooks. The
webhook guide is the handler, and the
event catalog is every payload.
| Resend event | Samva event | Difference worth keeping |
|---|---|---|
email.sent | The 201 is acceptance. message.sent is later. | Resend's email.sent means the API accepted the email and will try to deliver it. Samva's 201 is that moment. message.sent fires when the email is on its way to the recipient's server, and it does not mean the server accepted it. |
email.delivered | message.delivered | The recipient's mail server accepted the email. |
email.bounced | message.bounced | A permanent rejection. |
email.complained | message.complained | The recipient reported spam. |
email.failed | message.failed | Samva uses this when it did not send, or when a temporary rejection was abandoned. |
email.delivery_delayed | No equivalent event | A delay that later succeeds shows up as message.delivered. A temporary failure that is given up shows up as message.failed. |
email.opened | message.opened | Open tracking. |
email.clicked | message.clicked | Click tracking. |
email.received | message.received | Samva includes the conversation id. Fetch the email when you need headers or raw MIME. |
email.scheduled | No equivalent event | Read the scheduled message. Cancel it before it sends if the plan changes. |
email.suppressed | 422 ValidationError, or message.failed with errorCode: "SUPPRESSED" | An address already on the suppression list fails the request when every recipient is excluded. The webhook fires only after the message is accepted. |
domain.updated | domain.updated | Samva also emits domain.warming.updated. |
domain.created, domain.deleted | No equivalent events | Read the domain from the API when you need its current status. |
contact.created, contact.updated, contact.deleted | The same event names | Samva's contact payload is the contact after the change. |
contact.topics.updated | contact.unsubscribed, contact.resubscribed | Those events name an unsubscribe group, not a Resend topic. |
suppression.added, suppression.removed | No equivalent events | Add or lift an address from the dashboard. See suppression. |
Suppression has two outcomes, and a mixed envelope has a third. When every recipient is already
suppressed or unsubscribed, POST /v1/messages returns
422 ValidationError and fields names
each address and reason. That request accepts no message, so no webhook follows. When at least one
recipient remains, the request returns 201 and skippedRecipients lists the excluded addresses.
Those addresses do not get their own message.failed. After a message is accepted, a suppression
that lands before send emits message.failed with
errorCode: "SUPPRESSED". Handle the 422 on the send call. The
response format is the receipt.
message.received is the message event that carries conversationId. Delivery events do not.
Store webhook-id with the effect it guards. Samva retries a non-success response. The
webhook reference has the schedule.
Move the domain without taking Resend offline
Add the sending domain in Samva and publish the records it generates. Leave Resend's records in place until that provider is done sending. Verify your sending domain is the Samva workflow. The authentication guide is what SPF, DKIM, and DMARC each do.
| Record | Resend, checked October 16, 2026 | Samva |
|---|---|---|
| DKIM | A TXT record Resend generates. Copy it from the domain's Records tab. | A TXT record Samva generates for the domain. A second selector can exist beside Resend's. |
| SPF and return path | TXT and MX on the return-path host, or CNAME records for domains created after August 2026. The default host is send.example.com. A CNAME cannot share a name with another record. | MX and SPF TXT on mail.example.com. That host is the return path, not the address people see in From, and not an inbox. |
| DMARC | You publish the policy. Resend shows the parameters on the domain. | You publish the policy. Samva reads it and does not create or replace the record. |
| Receiving MX | An MX on the domain, or on a hostname you dedicate to receiving, routes inbound mail to Resend. | An MX on the domain when receiving is enabled. It does not block sending. A domain can be ready to send while that MX still points at Resend. |
Resend's domain setup also offers a sending region and enforced TLS. Those stay with the Resend domain. Samva's domain check is the DKIM record, the return-path records, and, once you turn receiving on, the inbound MX. Customer email requires TLS on the connection to the receiving server.
Do this before production mail moves:
- Compare the hostnames. Resend's default return path is
send.example.com(or the custom return path you set, plus anr-prefixed sibling when the records are CNAMEs). Samva's return path ismail.example.com. If either name is already taken, change the Resend return path or pick records that do not collide. A CNAME cannot sit on a name that already has TXT or MX. - Publish Samva's DKIM and return-path records beside Resend's. Do not delete Resend's records while Resend still sends.
- Wait until the Samva domain is ready to send. DNS often takes minutes and sometimes a few hours.
- Leave the receiving MX where it is. One hostname has one set of MX targets that matter. Moving it is the last step, after a test reply lands on Samva.
If Cloudflare or Vercel hosts the zone, automatic DNS setup can apply the Samva plan after you review it. Copy every value from the dashboard either way.
Move inbound mail onto conversations
Resend receives mail at a verified domain, or at the account's .resend.app subdomain. It parses
the message and POSTs email.received to the endpoint you choose. You then retrieve HTML, text,
headers, and attachments from the API. Resend's receiving docs also describe forwarding and
replying in the same thread. Its webhook catalog documents a separate inbox event set, including
threads, drafts, and assignments, and marks those inbox events private beta.
Samva's receiving path for this migration is message.received on a domain with receiving enabled.
The event includes the sender, your recipient addresses, the subject, attachment and auto-reply
flags, a parsed body when there is one, and the conversationId. It does not include a headers
map. Read get email details for messageIdHeader,
inReplyTo, references, and a signed URL for the raw MIME. Other headers you care about are in
that MIME, not in a headers field on the send API.
An inbound reply threads into the conversation of the email it answers. Samva does not gather every
email from one person into a single conversation. A send you make joins a conversation only when
you pass conversationId or email.inReplyToMessageId. The reply send still has no custom email
headers. Samva sets the threading headers from the message you name.
Return before you send when isAutoReply is true. That flag is on message.received. It covers
an out-of-office reply and other automatic replies. Each of those messages has its own messageId,
so the idempotency key reply:${messageId} does not stop the next automatic reply from starting
another send.
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
async function replyInConversation(event: {
messageId: string;
conversationId: string;
from: string;
subject?: string;
isAutoReply?: boolean;
}) {
if (event.isAutoReply) return;
const conversation = await samva.conversations.getById({ id: event.conversationId });
return samva.email.send(
{
conversationId: conversation.id,
to: event.from,
subject: event.subject ? `Re: ${event.subject}` : "Re: your message",
html: "<p>Thanks, we have your reply.</p>",
inReplyToMessageId: event.messageId,
},
{ idempotencyKey: `reply:${event.messageId}` },
);
}
Historical Resend messages do not import. Keep your ticket id, and attach the new message id and conversation id after the first Samva reply you trust.
Receive on a subdomain so the receiving MX does not take over a hostname that already runs mailboxes. The inbound guide is the shorter path if you are not moving a provider.
When the address belongs to an agent or an app
A mailbox is an address your application or agent owns. It can live on your organization's
samva.email subdomain, with no DNS, or on a verified domain that already receives mail. The agent
uses a key scoped to that mailbox. Sends can wait for a person to approve them, and quarantine can
hold a message until someone releases or discards it. Mailboxes is that
product. It is a different cutover from email.received: use it when the address itself is the
product, not when you are moving a transactional webhook.
Resend's private-beta inbox events are the closest description on that side. This guide does not map them field by field, because they are not the receiving API the rest of the page uses. If you depend on inbox assignment, folders, or drafts, write down which of those your application actually reads before you treat a mailbox as the replacement.
Cut over with a parallel run
Pick one flow. A receipt or a password reset is enough. Write the evidence you will keep, and the condition that sends the flow back to Resend.
| Checkpoint | Evidence to retain | Rollback condition |
|---|---|---|
| Headers | A written list of every custom header the Resend payload sets | Any header is required and has no replacement in your app |
| Domain | The Samva domain is ready to send, and Resend's records are still published | Verification is incomplete, or a return-path name collided |
| Send | Your record, the idempotency key, and the Samva message id agree | The new path cannot return or store a message id |
| Delivery | One signed Samva event is stored once for that message id | Delivered, bounced, or failed cannot be reconciled |
| Reply, when the flow has one | message.received carries a conversation id your app can read, and a test reply stays on that thread | The reply cannot be attached to the ticket or order |
| Rollback | The Resend call for this flow is still deployable | Any checkpoint fails during the observation window |
Run both providers only as alternative paths for that flow. Do not deliver the same production email through both of them to the same person. Use controlled test recipients for a side-by-side look, then send a small production slice through one path.
- Inventory the Resend call: from, recipients, template or HTML, headers, tags, schedule, topic, attachments, and the webhook effects your app applies.
- Stop if the call sets custom email headers you still need. Samva's send API cannot carry them.
- Verify the domain in Samva without removing Resend's DKIM, SPF, or return-path records.
- Point this flow at
samva.email.send, and keep the Resend function in the same deploy so you can switch the flow back. - Send a controlled test from the verified domain. Reconcile the message id with your record.
- Confirm
message.delivered, ormessage.bounced/message.failed, on the Samva endpoint. When every recipient is suppressed or unsubscribed, the request returns422 ValidationErrorand no delivery events follow. Other invalid requests also return this error. Inspect itsmessageandfieldson the send call to determine the reason. Leave the Resend endpoint and its verifier in place. - If the flow receives replies, send a controlled reply to the Resend receiving address first and
record what your app does today. Then, on a test hostname, enable Samva receiving and confirm
message.received, the conversation id, and a threaded reply. Do not change the production MX in this step. - Route the next small production slice of this one flow through Samva. Watch the event log and the support record.
- After the window you agreed is quiet, delete the Resend call for this flow only.
- When no remaining flow sends through Resend, remove Resend's sending records. Move the receiving MX last, onto a hostname you have already tested.
Record the window, the volume, and any event you could not explain. The migration is done when your application can account for every send, failure, and reply from its own records.
Known limits
- The send API has no custom email headers. A batch of distinct emails has no single request.
scheduled_at,tags,react, andtopic_iddo not copy across as the same fields. The map above is the replacement, or the statement that there is none. - This guide does not import historical Resend emails, events, contacts, or suppressions. Export what your policy says you must keep before you retire the account.
- Do not send one production recipient the same email from both providers to compare them.
- Inbound cutover needs its own rollback. Leave the Resend receiving MX in place until a controlled
reply has produced
message.receivedand a conversation your app can read. - Acceptance, delivery, and a recipient opening the email are different events. The proof is the message id you stored and the webhook you verified.
- Resend documents official SDKs for several languages, a CLI, and hosted MCP. Samva's SDK is
TypeScript. Other languages call
POST /v1/messages. The Python guide is that call. Hosted MCP and the CLI are on both products.
Continue from the first stable flow
The TypeScript SDK is the Promise client. The Effect SDK is the same operations as Effects. The REST API is the path for every other runtime. The email messaging model is how an immediate send, a scheduled send, and a campaign share one message id and one conversation.
Move the next flow only after this one can explain itself.
Sources
Checked October 16, 2026: