Switch Email SDKs
Keep your existing Resend, SendGrid, Mailjet, Postmark, or Mailgun SDK while moving transactional sending to Tinbox, with a full provider compatibility matrix.
Tinbox compat routes let you move transactional email into your own Cloudflare account without rewriting every send call first.
The point is not that you must keep provider SDKs forever. The point is that you can test-drive Tinbox before you touch your application code. Keep the SDK and dependencies you already ship, swap the API key, and change one host/base URL line. When you are ready, move to Tinbox-native APIs on your own schedule.
The simple version:
keep your current SDK
change the API key to a tbx_... key
change one SDK host/base URL line
send through Tinbox
Your app keeps the request shape it already uses. Tinbox translates that provider shape into the canonical Tinbox send pipeline: sender-scope checks, idempotency, suppressions, MIME building, dispatch, and delivery logs.
Every SDK path on this page is backed by a mirror test in
@tinbox/compat-sdk-smoke: the official SDK is pointed at localhost, the mirror
records the exact method, path, headers, content type, and body it posts, and the
compat route is built from that captured wire shape.
The code snippets are also represented in the smoke package as TypeScript
fixtures that import the official distributed SDK types. tsc --noEmit must
accept those fixtures for Resend, SendGrid, Mailjet, Postmark, and Mailgun before
we treat the guide as current.
Compat routes are for transactional sending. They do not clone provider dashboards, hosted templates, audience tools, contact APIs, or webhook signature systems. Those move separately.
Pick your SDK
Best fit: closest drop-in path. The Resend SDK supports baseUrl and RESEND_BASE_URL.
One-line trial: keep resend, change the key, and point baseUrl at Tinbox.
TINBOX_HOST=https://mail.yourdomain.com
TINBOX_API_KEY=tbx_...
RESEND_BASE_URL=https://mail.yourdomain.com/api/send/compat/resend/v1
import { Resend } from "resend";const resend = new Resend(process.env.TINBOX_API_KEY!, { baseUrl:
${process.env.TINBOX_HOST}/api/send/compat/resend/v1, });
await resend.emails.send({ from: "Acme <hello@yourdomain.com>", to: "ada@example.com", subject: "Your receipt", html: "<p>Thanks.</p>", });
Detailed guide: Switch from Resend.
Best fit: keep @sendgrid/mail for simple transactional sends. Configure the underlying client base URL and use a Tinbox key.
One-line trial: keep @sendgrid/mail, change the key, and set the client baseUrl to Tinbox.
import sgMail from "@sendgrid/mail"; import sgClient from "@sendgrid/client";sgMail.setApiKey(process.env.TINBOX_API_KEY!); sgClient.setDefaultRequest( "baseUrl",
${process.env.TINBOX_HOST}/api/send/compat/sendgrid, );
await sgMail.send({ from: "hello@yourdomain.com", to: "ada@example.com", subject: "Your receipt", html: "<p>Thanks.</p>", });
Compat route: POST /api/send/compat/sendgrid/v3/mail/send. Vendor-style alias: POST /v3/mail/send.
Detailed guide: Switch from SendGrid.
Best fit: keep node-mailjet. Mailjet's Messages[] body maps naturally to Tinbox batch sends.
One-line trial: keep node-mailjet, change the secret, and set config.host to Tinbox.
import Mailjet from "node-mailjet";const mailjet = new Mailjet({ apiKey: "tinbox", apiSecret: process.env.TINBOX_API_KEY!, config: { host: "mail.yourdomain.com/api/send/compat/mailjet", version: "v3.1" }, });
await mailjet.post("send", { version: "v3.1" }).request({ Messages: [{ From: { Email: "hello@yourdomain.com", Name: "Acme" }, To: [{ Email: "ada@example.com" }], Subject: "Your receipt", HTMLPart: "<p>Thanks.</p>", }], });
Compat route: POST /api/send/compat/mailjet/v3.1/send. Vendor-style alias: POST /v3.1/send.
Detailed guide: Switch from Mailjet.
Best fit: keep postmark for sendEmail and sendEmailBatch. Postmark uses a host-only config, so Tinbox exposes root aliases.
One-line trial: keep postmark, change the token, and set requestHost to Tinbox.
import postmark from "postmark";const client = new postmark.ServerClient(process.env.TINBOX_API_KEY!, { useHttps: true, requestHost: "mail.yourdomain.com", });
await client.sendEmail({ From: "hello@yourdomain.com", To: "ada@example.com", Subject: "Your receipt", HtmlBody: "<p>Thanks.</p>", });
Canonical compat routes are POST /api/send/compat/postmark/v1/email and POST /api/send/compat/postmark/v1/email/batch. The official SDK uses the root aliases POST /email and POST /email/batch.
Detailed guide: Switch from Postmark.
Best fit: keep mailgun.js for basic messages.create sends. The SDK smoke test verifies multipart/form-data posts to the canonical Tinbox compat prefix.
One-line trial: keep mailgun.js, change the key, and set the client url to Tinbox.
import Mailgun from "mailgun.js";const mailgun = new Mailgun(FormData); const mg = mailgun.client({ username: "api", key: process.env.TINBOX_API_KEY!, url:
${process.env.TINBOX_HOST}/api/send/compat/mailgun, });
await mg.messages.create("yourdomain.com", { from: "hello@yourdomain.com", to: ["ada@example.com"], subject: "Your receipt", html: "<p>Thanks.</p>", });
Compat route: POST /api/send/compat/mailgun/v3/:domain/messages. Vendor-style alias: POST /v3/:domain/messages.
Detailed guide: Switch from Mailgun.
What stays the same
For simple transactional email, most projects keep the same application-level send call:
- same
from,to,cc,bcc - same
subject - same
htmlandtext - same base64 attachments
- same reply-to intent
- same SDK-side ergonomics
What changes is the delivery plane. The API key becomes a Tinbox sending key, the host points at your Tinbox deployment, and Tinbox stores the send ledger in your database.
Compatibility levels
| Level | Meaning |
|---|---|
| L1 | Keep the official SDK. Change host/base URL and key only. |
| L2 | Keep the official SDK, with route aliases or small constructor config. |
| L3 | Keep the request body over fetch; official SDK targeting is imperfect. |
| L4 | Use a small app adapter because the provider feature is too different. |
Provider routes
Every provider gets a Tinbox-owned compat path under /api/send/compat. Some
providers also get vendor-style aliases so official SDKs with host-only config
can still work.
The official SDK mirror suite currently validates these package versions:
| Provider | SDK package | Tested version | SDK path mode |
|---|---|---|---|
| Resend | resend | 6.17.2 | canonical /api/send/compat/... prefix |
| SendGrid | @sendgrid/mail + @sendgrid/client | 8.1.6 | canonical /api/send/compat/... prefix |
| Mailjet | node-mailjet | 6.0.11 | canonical /api/send/compat/... prefix |
| Postmark | postmark | 5.0.0 | root aliases (/email, /email/batch) |
| Mailgun | mailgun.js | 13.2.0 | canonical /api/send/compat/... prefix |
| Provider | Canonical Tinbox compat route | Vendor-style alias |
|---|---|---|
| Resend | POST /api/send/compat/resend/v1/emails | POST /api/send/emails |
| Resend batch | POST /api/send/compat/resend/v1/emails/batch | POST /api/send/emails/batch |
| SendGrid | POST /api/send/compat/sendgrid/v3/mail/send | POST /v3/mail/send |
| Mailjet | POST /api/send/compat/mailjet/v3.1/send | POST /v3.1/send |
| Postmark | POST /api/send/compat/postmark/v1/email | POST /email |
| Postmark batch | POST /api/send/compat/postmark/v1/email/batch | POST /email/batch |
| Mailgun | POST /api/send/compat/mailgun/v3/:domain/messages | POST /v3/:domain/messages |
| Mailgun raw MIME | POST /api/send/compat/mailgun/v3/:domain/messages.mime | POST /v3/:domain/messages.mime |
Full parity matrix
| Feature | Resend | SendGrid | Mailjet | Postmark | Mailgun |
|---|---|---|---|---|---|
| Compat level | L1 | L1/L2 | L1/L2 | L2 | L2/L3 |
| Wire API version | v1-style /emails | v3 | v3.1 | v1-style /email | v3 |
| Keep official SDK | Yes | Yes, for simple sends | Yes | Yes, with root aliases | Partial |
| Host/base URL override | baseUrl, RESEND_BASE_URL | setDefaultRequest("baseUrl") | config.host | requestHost | url |
| Tinbox route | /api/send/compat/resend/v1/emails | /api/send/compat/sendgrid/v3/mail/send | /api/send/compat/mailjet/v3.1/send | /api/send/compat/postmark/v1/email | /api/send/compat/mailgun/v3/:domain/messages |
| Auth accepted | Bearer tbx_... | Bearer tbx_... | Basic password or bearer | X-Postmark-Server-Token or bearer | Basic password or bearer |
| Payload | JSON | JSON | JSON | JSON | multipart/form-data |
| Single send | Supported | Supported | Supported | Supported | Supported |
| Batch send | Supported if SDK route matches | Partial via personalizations | Supported via Messages[] | Supported | Partial/client loop |
to / cc / bcc | Supported | Supported | Supported | Supported with comma parsing | Supported |
| Reply-To | Supported | Supported | Supported | Supported | Supported via h:Reply-To |
| HTML + text | Supported | Supported via content parts | Supported | Supported | Supported |
| Custom headers | Partial, X-* only | Partial, X-* only | Partial, X-* only | Partial, X-* only | Partial, h:X-* only |
| Idempotency | Supported via Idempotency-Key | Partial via explicit Tinbox custom arg/header | Partial via CustomID or header | Partial via metadata or header | Partial via variable/header |
| Base64 attachments | Supported | Supported | Supported | Supported | Supported after form-file conversion |
| Path/URL attachments | Unsupported unless SDK pre-converts | Unsupported unless SDK pre-converts | Unsupported | Unsupported | Not applicable |
| Inline/CID attachments | Not yet | Not yet | Not yet | Not yet | Not yet |
| Hosted templates | Not a compat feature | Not a compat feature | Not a compat feature | Not a compat feature | Not a compat feature |
| Scheduled sends | Not a compat feature | Not a compat feature | Not a compat feature | Not a compat feature | Not a compat feature |
| Tags / metadata | Partial/ignored | Partial, later compat metadata | Partial, CustomID only | Partial, later metadata | Partial, later variables |
| Suppression APIs | Use Tinbox native | Use Tinbox native | Use Tinbox native | Use Tinbox native | Use Tinbox native |
| Webhooks/events | Use Tinbox native | Use Tinbox native | Use Tinbox native | Use Tinbox native | Use Tinbox native |
| Success response | Resend SDK wrapper over Tinbox body | 202 + message header | Mailjet Messages[] | Postmark message response | Mailgun queued response |
Compatibility warnings
Dangerous gaps fail fast with 422: hosted templates, scheduled sends, and raw
MIME routes that Tinbox does not support yet. Lower-risk degraded fields are
accepted but return an X-Tinbox-Compat-Warnings header. The response body stays
provider-shaped so official SDKs keep parsing it.
Examples of warning-only fields:
| Provider | Warning examples |
|---|---|
| Resend | tags |
| SendGrid | extra personalizations[], asm, mail_settings, tracking_settings, categories |
| Mailjet | InlinedAttachments, EventPayload, MonitoringCategory |
| Postmark | MessageStream, Tag, Metadata, TrackOpens, TrackLinks |
| Mailgun | non-X-* h:* headers, tracking/campaign options, recipient variables |
SDK gaps at a glance
These are the gaps that should be visible before a user changes production traffic. Compat routes should either reject these fields clearly or document that the value is ignored.
| SDK | Main gaps | What to do instead |
|---|---|---|
| Resend | scheduledAt, hosted templates, audiences/broadcasts/topics, Resend webhook verification, attachment paths when not pre-converted | Schedule in your app, render templates before send, migrate events to Tinbox logs/webhooks, pass base64 attachments |
| SendGrid | template_id, dynamic_template_data, send_at, ASM unsubscribe groups, tracking settings, advanced substitutions, non-X-* headers | Render templates before send, use Tinbox audiences/suppressions, use Tinbox events, keep custom mail headers under X-* |
| Mailjet | TemplateID, Variables, InlinedAttachments, sandbox mode, Mailjet event payload semantics | Render templates before send, avoid inline/CID attachments for now, use Tinbox test flows and event logs |
| Postmark | template endpoints, MessageStream, Tag/Metadata analytics parity, content IDs, non-X-* headers | Use /email with rendered content, use Tinbox logs for delivery visibility, keep custom mail headers under X-* |
| Mailgun | raw MIME passthrough, inline files, recipient variables, tags/campaign analytics, stored templates | Use basic /messages first, render variables/templates in your app, use Tinbox events and logs |
What does not move through compat routes
Compat routes are deliberately narrow. These should be migrated to Tinbox-native features or app code:
- provider-hosted templates
- scheduled send APIs
- broadcasts, audiences, contacts, and marketing list APIs
- provider webhook signature verification
- provider analytics dashboards
- domain and sender management APIs
- raw MIME passthrough
Recommended migration path
- Deploy Tinbox and verify your sending domain.
- Create a Tinbox sending API key scoped to your app's
fromaddress or domain. - Pick your current SDK above and change only the key and one host/base URL line.
- Send a smoke test from the same
fromaddress production uses. - Check the Tinbox send ledger and the received email's SPF/DKIM/DMARC headers.
- Move advanced provider features one by one after simple sends are stable.