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>",
});| Field | Type | Notes |
|---|---|---|
tags | translated | Their [{ name, value }] becomes our name:value strings. |
attachments[].content_type | translated | Becomes our type. |
attachments[].content_id | translated | Becomes contentId and marks the file inline. |
attachments[].path | refused | We do not fetch attachments from a URL. Send base64 content. |
scheduled_at | translated | Becomes scheduled_for. Must be an ISO 8601 instant — relative times like "in 1 min" are refused rather than silently sent now. |
template | translated | template.id must be a Transmit template id; variables become our variables. Recreate the template here first. |
topic_id | refused | Resend Topics are not a Transmit product. Drop it from the send. |
batch.send | translated | POST /emails/batch. Same bodies as send, up to 100. Attachments are refused. Strict by default; x-batch-validation: permissive queues what it can. |
Idempotency-Key | optional | Not 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.",
});| Field | Type | Notes |
|---|---|---|
personalizations | translated | Each becomes one message, with its own recipients and subject. |
content[] | translated | text/plain becomes text, text/html becomes html. |
categories | translated | Becomes tags. |
send_at | translated | A unix timestamp becomes scheduled_for. |
dynamic_template_data | translated | Becomes variables. |
template_id | refused | A 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/emailsrequire anIdempotency-Keyheader (up to 255 characters). Requests without one are rejected with400 invalid_idempotency_key. (Our official SDK generates one per call automatically when omitted). - Read the message ID from
id. The send endpoint answers202 Acceptedwith a JSON receipt containing{ id }. Webhooks correlate back to this send asdata.email_id. If an existing adapter looks formessage_id,messageId, or anX-Message-Idheader, point it atresponse.id. - Attachments, inline images, and tags are supported. Transmit supports inline base64 attachments and large uploads via
POST /v1/attachments. Setdisposition: "inline"with acontentIdfor inline images, and pass up to 50 string labels intags. - 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
broadcastor 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.