Skip to content
SendGrid

Migrate SendGrid Inbound Parse without losing replies

Move SendGrid Inbound Parse to Samva with a payload map, verified webhooks, attachment retrieval, duplicate handling, and a reversible MX cutover.

Published

Move one receiving hostname first, such as replies.example.com. Keep your SendGrid handler available while you prove the Samva path, then change that hostname's MX records. Outbound sending can stay on SendGrid throughout this migration.

This guide is for product teams already using SendGrid Inbound Parse. For the product decision, start with Samva vs SendGrid. For a new integration, use the shorter inbound email guide.

Map the Fields Your Application Actually Uses

SendGrid posts multipart form data. Samva posts a JSON event with type, timestamp, and data. The event signals that a message was received; fetch email details when you need headers, separate body formats, or attachment metadata.

SendGrid inputSamva destinationMigration decision
from, to, subjectdata.from, data.to, data.subjectKeep sender display names separate from addresses. data.toEmails lists recipients routed to this endpoint.
envelope.toRouted recipients in data.toEmailsRecheck alias and ticket routing. Header recipients and SMTP envelope recipients are different concepts.
text, htmldata.body, or email content URLsbody can be HTML or text; it is not two separate fields. Fetch the explicit text/HTML representation if the distinction matters.
headersmessages.getEmail: messageIdHeader, inReplyTo, referencesUse these to reconcile existing application threads. Fetch raw MIME for other headers.
attachments, attachment-info, file partshasAttachments, attachmentCount, attachments, raw MIMEMetadata is separate from bytes. Retrieve the raw email to extract attachments.
Raw-mode emails3ContentUrls.rawMime from messages.getEmailDownload the MIME message using a fresh signed URL.
SPF, dkimspfVerdict, dkimVerdict, dmarcVerdict on email detailsReview your policy against the verdict values; webhook authentication does not authenticate the email author.
spam_score, spam_reportNo equivalent fields in the public email responseA workflow depending on these values needs its own replacement before cutover.

The SendGrid field names and raw-mode distinction follow its Inbound Parse payload documentation, checked September 22, 2026. Samva's get email details reference shows the complete response. Do not treat missing fields as empty content.

Keep Authentication Separate for Each Endpoint

Keep /webhooks/sendgrid and /webhooks/samva as separate routes during the transition. SendGrid supports ECDSA signatures, OAuth, or both through an Inbound Parse security policy. Its signature headers are X-Twilio-Email-Event-Webhook-Signature and X-Twilio-Email-Event-Webhook-Timestamp, even for Inbound Parse. Validate the original multipart bytes before a framework extracts form fields or uploaded files. Keep the existing public key and OAuth policy active while old deliveries drain. See SendGrid's Inbound Parse security instructions.

For Samva, register an endpoint subscribed to message.received and store its returned signing secret. Pass the untouched request to verifyRequest, which verifies the signature and timestamp before decoding JSON. Do not reuse the SendGrid key or run its verifier on the Samva route.

Install samva and zod for this example. The handler factory deliberately takes your durable inbox operation as an argument: wire it to a database transaction that inserts the event under a unique (organization, provider, eventId) key and enqueues the worker in the same transaction. A duplicate should succeed without a second job. A failed transaction must reject so delivery can retry. The organization comes from your endpoint configuration, never an untrusted body field.

import { verifyRequest, WebhookVerificationError } from "samva/webhooks";
import { z } from "zod";

const receivedEmail = z.object({
  messageId: z.string().min(1),
  conversationId: z.string().min(1),
  from: z.string().min(1),
  to: z.string().min(1),
  toEmails: z.array(z.string().min(1)).min(1),
  hasAttachments: z.boolean(),
  isAutoReply: z.boolean(),
});

type InboxItem = {
  eventId: string;
  email: z.infer<typeof receivedEmail>;
};

export function createInboundHandler(
  secret: string,
  acceptOnce: (item: InboxItem) => Promise<void>,
) {
  return async function POST(request: Request): Promise<Response> {
    let verified;
    try {
      verified = await verifyRequest(request, secret);
    } catch (error) {
      if (error instanceof WebhookVerificationError) {
        return new Response("Invalid webhook", { status: 401 });
      }
      throw error;
    }
    if (verified.event.type !== "message.received") {
      return new Response(null, { status: 204 });
    }
    const email = receivedEmail.parse(verified.event.data);
    await acceptOnce({ eventId: verified.id, email });
    return new Response(null, { status: 204 });
  };
}

This schema validates the fields this worker requires; it is not the complete event catalog. An unexpected payload or failed inbox write must become a non-2xx response in your framework, with an alert and enough protected diagnostic context to recover it. Do not catch processing failures and return success. Keep automatic responses out of this request handler. In the worker, check toEmails against your accepted-address rules before assigning a ticket. Record isAutoReply messages but suppress automated replies to them.

Retrieve Attachment Bytes and the Exact Body Format

Use the received messageId to read email details. The attachments array contains metadata, including each attachment's filename, size, content type, and inline content ID. The webhook does not upload file parts to your endpoint. For a public SDK integration, download raw MIME and pass those bytes to your MIME parser to extract attachments, including inline images.

import { createClient } from "samva";

const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });

export async function readInboundEmail(messageId: string) {
  const email = await samva.messages.getEmail({
    id: messageId,
    includeS3Urls: "true",
    urlExpiresIn: "300",
  });
  const rawUrl = email.s3ContentUrls?.rawMime;
  if (!rawUrl) throw new Error("Raw email unavailable; keep the inbox job retryable");
  const response = await fetch(rawUrl);
  if (!response.ok) throw new Error(`Raw email download failed: ${response.status}`);
  return { email, rawMime: new Uint8Array(await response.arrayBuffer()) };
}

Treat the signed URL as a temporary credential: fetch it on the server, do not log it, and request a new URL if it expires. Persist attachment bytes or a durable processing result before completing the inbox job. Apply your file-size, malware, filename, and content-type policies before exposing files. Sanitize HTML before rendering it. Use s3ContentUrls.text or s3ContentUrls.html when you need one body format; either can be null. The API option includeS3Urls is the literal SDK field name for these content URLs.

Separate Retries from Message and Ticket Identity

A Samva webhook-id identifies an event, data.messageId identifies a stored message, and data.conversationId identifies its conversation. An email's RFC Message-ID header is a fourth identifier. Keep all four in distinct columns.

SituationIdentity and action
Samva retries the same eventDeduplicate verified.id. Retries keep it but refresh timestamp and signature.
Two events refer to the same messageMake the downstream business effect unique by organization, message ID, and action. Event deduplication alone does not cover this.
The old and new routes both observe an emailReconcile the RFC Message-ID, receiving address, and existing ticket record. Provider event IDs cannot deduplicate across providers.
A sender omits or reuses Message-IDQuarantine ambiguous matches for review; do not collapse mail solely on subject or sender.
A worker fails after accepting a webhookRetry the durable inbox job. The provider stops retrying once the endpoint returns 2xx.

Samva delivery is at least once and unordered. Return 2xx after durable acceptance and do slower attachment and ticket work in the worker. Its retry schedule and redelivery semantics differ from SendGrid's. SendGrid documents retries for unsuccessful deliveries. Monitor the old queue during the drain window using SendGrid's delivery guidance.

Keep your existing ticket ID as the application authority. When a newly received message belongs to that ticket, add its Samva conversation ID to the mapping after checking the receiving address and reply headers. Migration does not import historical SendGrid messages or preserve an old provider's conversation identifiers. A Samva conversation is not automatically your ticket. The reply example shows conversationId and inReplyToMessageId for a reply to a message already stored in Samva.

Cut Over One Hostname with a Rollback Record

  1. Inventory the current route. Save the SendGrid receiving hostname, destination URL, raw-mode and spam-check settings, security policy, and exact MX values, priorities, and TTL. Record every alias and recipient rule your application expects. Keep outbound authentication records unchanged when only moving inbound routing.
  2. Prove a separate receiving subdomain. Configure a verified Samva domain and endpoint using configure receiving. Use a test hostname before the existing production hostname. Receiving is catch-all for that domain; enforce your application's accepted-address rules after verification.
  3. Exercise the migration checklist below. Use controlled test mail and local signed fixtures. A webhook test alone does not prove SMTP routing, MIME parsing, or attachment availability.
  4. Prepare DNS. Lower the receiving hostname's TTL in advance and wait out its previous TTL. Confirm Samva receiving is enabled for the exact hostname. Copy the MX target and priority shown for that domain; do not guess a regional hostname. Avoid an automatic DNS apply that would move traffic before you are ready.
  5. Replace that hostname's MX route. SendGrid's documented target is mx.sendgrid.net. Replace the saved route with the Samva records. MX priority is failover preference, not a percentage rollout or a way to duplicate every email to both services. Leave your primary mailbox domain alone if you are moving only a reply subdomain.
  6. Observe and retain both handlers. Inspect authoritative and public-resolver MX answers, then reconcile controlled received mail through signature verification, durable inbox storage, body/attachment retrieval, and ticket mapping. Keep SendGrid accepting queued webhooks for at least its documented three-day retry window after its last received message, and longer if old-route arrivals or unresolved work remain. DNS caches and sender queues can outlast a TTL.
  7. Rollback on missing or misrouted mail. Restore the saved MX records, keep both webhook handlers and workers running, and reconcile mail already accepted by either provider. DNS rollback affects future routing; it does not move already stored messages or cancel jobs. Retire the old endpoint only after the queues, application records, and observation window agree.

Prove These Cases Before Moving Customer Replies

CaseRequired result
Valid and tampered webhookValid event reaches the durable inbox; changed body or missing signature cannot create work.
Concurrent duplicate deliveriesOne inbox job; a fresh retry signature still maps to the same event.
Inbox write fails, then recoversFirst request is non-2xx; a retry can persist and process it.
Plain text, HTML, Unicode, and raw-mode source mailIntended content survives; absent body formats remain absent.
File attachment and inline imageMIME extraction preserves expected bytes, filename, media type, and content ID.
Reply to a pre-migration ticketExisting ticket mapping is explicit; a new Samva ID does not silently create a duplicate ticket.
Out-of-office replyMessage is recorded without sending another automated reply.
MX rollback with work in flightBoth queues drain; accepted mail remains recoverable and business effects do not repeat.

Start with receiving setup, then use this checklist as your migration acceptance record. Keep the old route until it proves the behavior your application needs.

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.