Skip to content
Resend

Migrate from Resend and keep replies in the thread

Map Resend sends, batches, templates, webhooks, and domains onto Samva, then cut over one flow while replies stay in the conversation.

Published

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 fieldSamva destinationWhat to decide
fromfrom: { email, name }, optionalResend 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, bccto, cc, bccA string, { email }, or { contactId }. Resend caps to at 50 addresses.
subject, html, textsubject plus html or textInline content needs a subject and at least one body. A template send omits all three.
react (Node.js SDK)No react fieldSend HTML, or publish a template and pass its slug. The send call does not accept a React component.
reply_toreplyToAn array of addresses, or null.
headersNo fieldThe send payload has no custom email headers. See below.
tagsmetadataA 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_atscheduledMessages.createmessages.send has no scheduling field. The scheduled message is a separate resource you can read or cancel.
attachmentsattachmentsMedia you have uploaded, or bounded base64 content. Resend's batch endpoint does not accept attachments; a single Resend send does.
template.id, template.variablestemplateSlug or templateId, plus templateDataName one published template. Do not also send subject, html, or text.
Idempotency-KeyIdempotency-KeyResend 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_idunsubscribeGroupId on the email, when you mean a preferenceA 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 eventSamva eventDifference worth keeping
email.sentThe 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.deliveredmessage.deliveredThe recipient's mail server accepted the email.
email.bouncedmessage.bouncedA permanent rejection.
email.complainedmessage.complainedThe recipient reported spam.
email.failedmessage.failedSamva uses this when it did not send, or when a temporary rejection was abandoned.
email.delivery_delayedNo equivalent eventA delay that later succeeds shows up as message.delivered. A temporary failure that is given up shows up as message.failed.
email.openedmessage.openedOpen tracking.
email.clickedmessage.clickedClick tracking.
email.receivedmessage.receivedSamva includes the conversation id. Fetch the email when you need headers or raw MIME.
email.scheduledNo equivalent eventRead the scheduled message. Cancel it before it sends if the plan changes.
email.suppressed422 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.updateddomain.updatedSamva also emits domain.warming.updated.
domain.created, domain.deletedNo equivalent eventsRead the domain from the API when you need its current status.
contact.created, contact.updated, contact.deletedThe same event namesSamva's contact payload is the contact after the change.
contact.topics.updatedcontact.unsubscribed, contact.resubscribedThose events name an unsubscribe group, not a Resend topic.
suppression.added, suppression.removedNo equivalent eventsAdd 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.

RecordResend, checked October 16, 2026Samva
DKIMA 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 pathTXT 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.
DMARCYou 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 MXAn 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:

  1. Compare the hostnames. Resend's default return path is send.example.com (or the custom return path you set, plus an r-prefixed sibling when the records are CNAMEs). Samva's return path is mail.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.
  2. Publish Samva's DKIM and return-path records beside Resend's. Do not delete Resend's records while Resend still sends.
  3. Wait until the Samva domain is ready to send. DNS often takes minutes and sometimes a few hours.
  4. 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.

CheckpointEvidence to retainRollback condition
HeadersA written list of every custom header the Resend payload setsAny header is required and has no replacement in your app
DomainThe Samva domain is ready to send, and Resend's records are still publishedVerification is incomplete, or a return-path name collided
SendYour record, the idempotency key, and the Samva message id agreeThe new path cannot return or store a message id
DeliveryOne signed Samva event is stored once for that message idDelivered, bounced, or failed cannot be reconciled
Reply, when the flow has onemessage.received carries a conversation id your app can read, and a test reply stays on that threadThe reply cannot be attached to the ticket or order
RollbackThe Resend call for this flow is still deployableAny 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.

  1. Inventory the Resend call: from, recipients, template or HTML, headers, tags, schedule, topic, attachments, and the webhook effects your app applies.
  2. Stop if the call sets custom email headers you still need. Samva's send API cannot carry them.
  3. Verify the domain in Samva without removing Resend's DKIM, SPF, or return-path records.
  4. Point this flow at samva.email.send, and keep the Resend function in the same deploy so you can switch the flow back.
  5. Send a controlled test from the verified domain. Reconcile the message id with your record.
  6. Confirm message.delivered, or message.bounced / message.failed, on the Samva endpoint. When every recipient is suppressed or unsubscribed, the request returns 422 ValidationError and no delivery events follow. Other invalid requests also return this error. Inspect its message and fields on the send call to determine the reason. Leave the Resend endpoint and its verifier in place.
  7. 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.
  8. Route the next small production slice of this one flow through Samva. Watch the event log and the support record.
  9. After the window you agreed is quiet, delete the Resend call for this flow only.
  10. 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, and topic_id do 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.received and 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:

Frequently Asked Questions

Related Resources

Send

Ship your first email today.

Transactional and product email through one typed API, with signed events, conversation threading, and deliverability handled.