Moving in

Migrating

We answer Resend and SendGrid request shapes at the paths their clients already call. Change the base URL and the key; leave the rest.

From Resend

POST /emails takes their body and answers { id }, as they do. Their own client works unchanged:

Their SDK, our APIimport { Resend } from "resend";

const resend = new Resend(process.env.TRANSMIT_API_KEY, {
  baseUrl: "https://api.transmit.dev",   // the only line that changes
});

await resend.emails.send({
  from: "Acme <hello@yourdomain.com>",
  to: ["you@example.com"],
  subject: "It arrived.",
  html: "<p>Nobody noticed.</p>",
});
What we do with the fields that differ
FieldTypeNotes
tagstranslatedTheir [{ name, value }] becomes our name:value strings.
attachments[].content_typetranslatedBecomes our type.
attachments[].content_idtranslatedBecomes contentId and marks the file inline.
attachments[].pathrefusedWe do not fetch attachments from a URL. Send base64 content.
scheduled_attranslatedBecomes scheduled_for. Must be an ISO 8601 instant — relative times like "in 1 min" are refused rather than silently sent now.
templatetranslatedtemplate.id must be a Transmit template id; variables become our variables. Recreate the template here first.
topic_idrefusedResend Topics are not a Transmit product. Drop it from the send.
batch.sendtranslatedPOST /emails/batch. Same bodies as send, up to 100. Attachments are refused. Strict by default; x-batch-validation: permissive queues what it can.
Idempotency-KeyoptionalNot required here, and generated per request when absent.

From SendGrid

POST /v3/mail/send takes their body and answers 202 with the id in X-Message-Id, as they do.

Their SDK, our APIimport sgMail from "@sendgrid/mail";

sgMail.setApiKey(process.env.TRANSMIT_API_KEY);
sgMail.client.setDefaultRequest("baseUrl", "https://api.transmit.dev");  // the only line that changes

await sgMail.send({
  to: "you@example.com",
  from: "hello@yourdomain.com",
  subject: "It arrived.",
  text: "Nobody noticed.",
});
What we do with the fields that differ
FieldTypeNotes
personalizationstranslatedEach becomes one message, with its own recipients and subject.
content[]translatedtext/plain becomes text, text/html becomes html.
categoriestranslatedBecomes tags.
send_attranslatedA unix timestamp becomes scheduled_for.
dynamic_template_datatranslatedBecomes variables.
template_idrefusedA d-… id names a template living in SendGrid. Recreate it here and send its id.

Where we cannot honour something, we refuse it loudly rather than dropping it. A scheduled message that silently sends immediately is worse than an error, so that case is a 422 naming the field.

Custom adapters and other providers

If you are adapting an existing in-house email client or migrating from providers like Mailgun:

  • Send an Idempotency-Key. Direct calls to POST /v1/emails require an Idempotency-Key header (up to 255 characters). Requests without one are rejected with 400 invalid_idempotency_key. (Our official SDK generates one per call automatically when omitted).
  • Read the message ID from id. The send endpoint answers 202 Accepted with a JSON receipt containing { id }. Webhooks correlate back to this send as data.email_id. If an existing adapter looks for message_id, messageId, or an X-Message-Id header, point it at response.id.
  • Attachments, inline images, and tags are supported. Transmit supports inline base64 attachments and large uploads via POST /v1/attachments. Set disposition: "inline" with a contentId for inline images, and pass up to 50 string labels in tags.
  • Suppression handling. Hard bounces, complaints, and one-click unsubscribes are recorded automatically. Provider-recorded evidence cannot be removed by self-service. To seed existing suppression lists from Mailgun or request a bounce reset, contact support.
  • Pacing and warmup. Workspaces default to 10 sends/second and 10,000/day. If sending bulk mail on broadcast or migrating pre-warmed domains, contact support in advance to adjust limits and warmup schedules.

What you still have to do

  • Verify your domain here. Sending domains do not travel; publish our DNS records before you cut over.
  • Recreate templates. A template in their system is not a template in ours.
  • Repoint webhooks. Our events and signature are our own — see webhooks.
  • Bring your suppression list. People who asked the old sender to stop have not changed their minds.

Once you are moved in, the TypeScript SDK reaches the rest of the API — templates, SMS, contacts, campaigns, analytics — which the compatibility shapes do not cover.