# Migrate from SendGrid

> Migrate from SendGrid without a rewrite: keep @sendgrid/mail, point it at api.emailfast.dev with a new key, and keep your 202 + X-Message-Id contract.

Canonical: https://emailfast.dev/migrate/sendgrid

## Two ways to switch

Email Fast speaks SendGrid's send API natively: SendGrid-, Mailgun-, and Postmark-compatible endpoints: point your existing SDK at a new base URL with a new key and keep your code. A successful call
returns what your code already expects: an empty-body `202` with an `X-Message-Id`
header.

### Path 1 — keep @sendgrid/mail

```diff
  import sgMail from "@sendgrid/mail";
+ import sgClient from "@sendgrid/client";

+ sgClient.setDefaultRequest("baseUrl", "https://api.emailfast.dev");
+ sgMail.setClient(sgClient);
- sgMail.setApiKey(process.env.SENDGRID_API_KEY);
+ sgMail.setApiKey(process.env.EMAILFAST_API_KEY); // ef_sandbox_… works everywhere

  await sgMail.send({
    to: "ada@example.com",
    from: "you@yourdomain.com",
    subject: "Welcome",
    html: "<h1>Hi Ada</h1>",
+   headers: { "Idempotency-Key": "welcome-ada-1" }, // retry-safe (see below)
  });
```

### Path 2 — adopt @email-fast/nodejs

```diff
- import sgMail from "@sendgrid/mail";
+ import { EmailFast } from "@email-fast/nodejs";

- sgMail.setApiKey(process.env.SENDGRID_API_KEY);
- await sgMail.send({
-   to: "ada@example.com",
-   from: "you@yourdomain.com",
-   subject: "Welcome",
-   html: "<h1>Hi Ada</h1>",
- });
+ const ef = new EmailFast({ apiKey: process.env.EMAILFAST_API_KEY, baseUrl: "https://api.emailfast.dev" });
+ await ef.send({
+   to: "ada@example.com",
+   subject: "Welcome",
+   html: "<h1>Hi Ada</h1>",
+   idempotency_key: "welcome-ada-1",
+ });
```

## What maps 1:1

- **Personalizations.** Each entry in `personalizations` fans out to its own
  message, exactly as SendGrid does. If one recipient is blocked by policy, the
  whole call rolls back and returns a truthful 4xx — nothing half-queued.
- **Templates.** `template_id` resolves against your imported template, so
  dynamic-template sends keep working after import.
- **The response contract.** `202`, empty body, `X-Message-Id` header.

Every message admitted this way passes the same checkpoint as every other ingress:
every send — REST, SMTP, browser SDK, compatibility endpoints, broadcasts, automations — passes through one admission gate: idempotency, suppression, quota, and content policy in a single checkpoint no ingress can skip.

## What arms at launch

The hosted data importers — templates, contacts, and suppression lists pulled
across from your SendGrid account — arm at launch. Today you can evaluate the
whole send path with a sandbox key: sandbox keys (ef_sandbox_…) that run the real pipeline dry: real validation, real rendering, real events, a hosted capture inbox — and no email leaves.

## The idempotency bonus

SendGrid's send endpoint has no idempotency. Ours does: pass an `Idempotency-Key`
header and each recipient in a personalizations fan-out gets its own derived key,
so a timeout-and-retry can never double-send part of a batch: a 202 from the API means the send is committed to a durable, partitioned outbox before we answer — a crash can't lose it, and a retry with the same idempotency key can't double-send.

## Next

- [Why teams leave SendGrid](/compare/sendgrid-alternative)
- [The full migration kit](/features/migration)
- [API docs](/docs)

## Do I need to change my application code to migrate from SendGrid?

No. POST /v3/mail/send is accepted as-is: same Bearer auth, same body shape, same 202-with- X-Message-Id response. You change the base URL and the key.

## Do my SendGrid dynamic templates work?

Yes, once imported. A send that references a template_id resolves against your imported copy of that template; an unknown id returns a SendGrid-shaped 400 telling you to import it first.

## How do I test before pointing production at Email Fast?

Use a sandbox key ( ef_sandbox_… ) in the same code. The full pipeline runs — validation, rendering, events, a hosted capture inbox — and no email leaves.
