# Migrate from SendGrid

> Move from SendGrid to Emailit. Map concepts, API fields, SMTP settings and Event Webhook names, then bring over suppressions, dynamic templates and DNS.

This guide maps SendGrid concepts, API calls, webhooks, suppressions and templates to their Emailit equivalents. Read [Migrate to Emailit](/docs/migrate/) first for the overall order and how to run both providers in parallel.

## Concepts

| SendGrid | Emailit |
| --- | --- |
| Account and subusers | Account and [workspaces](/docs/workspaces/). Each workspace has its own domains, keys, members and credits. |
| API key with permissions | [API key](/docs/developers/api-keys/): **Full Access**, or **Sending Only** optionally restricted to one domain |
| Domain authentication | [Sending domain](/docs/domains/) with SPF, DKIM and return path records |
| Link branding | [Tracking subdomain](/docs/tracking/), a CNAME such as `go.acme.com` |
| Single sender verification | Not available. Every From address must be on a verified domain. |
| Dynamic templates | [Templates](/docs/templates/) with an alias and versions, rendered with [Temple](/docs/templates/temple/) |
| Event Webhook | [Webhooks](/docs/webhooks/) |
| Inbound Parse | [Inbound email](/docs/inbound/) |
| Suppressions | [Suppressions](/docs/suppressions/) |
| Unsubscribe groups | Not available. Use [audiences](/docs/audiences/) and campaign unsubscribe links. |
| Marketing contacts and lists | [Contacts](/docs/contacts/) and [audiences](/docs/audiences/) |
| Single Sends | [Campaigns](/docs/campaigns/) |
| Email Activity | **Email API → Emails** and **Email API → Logs** |
| Categories and custom args | `meta` |
| Dedicated IPs and IP pools | [Dedicated IPs](/docs/deliverability/dedicated-ips/) on request |
| Email address validation | [Email verification](/docs/email-verification/) |

## Update your API calls

SendGrid's `POST /v3/mail/send` becomes `POST /v2/emails`. The request is flatter: there are no `personalizations`, and addresses are plain strings.

```bash title="Before: SendGrid"
curl https://api.sendgrid.com/v3/mail/send \
  -H "Authorization: Bearer $SENDGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "personalizations": [{ "to": [{ "email": "ada@example.com" }] }],
    "from": { "email": "hello@acme.com", "name": "Acme" },
    "subject": "Your receipt",
    "content": [
      { "type": "text/plain", "value": "Thanks for your order." },
      { "type": "text/html", "value": "<p>Thanks for your order.</p>" }
    ]
  }'
```

```bash title="After: Emailit"
curl https://api.emailit.com/v2/emails \
  -H "Authorization: Bearer $EMAILIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <hello@acme.com>",
    "to": "ada@example.com",
    "subject": "Your receipt",
    "text": "Thanks for your order.",
    "html": "<p>Thanks for your order.</p>"
  }'
```

| SendGrid | Emailit |
| --- | --- |
| `Authorization: Bearer SG.…` | `Authorization: Bearer secret_…` |
| `from: { email, name }` | `from: "Name "` |
| `personalizations[].to[]` | `to`, a string or an array of up to 50 addresses |
| `personalizations[].cc[]`, `bcc[]` | `cc`, `bcc` |
| `reply_to: { email }` | `reply_to` |
| `subject` | `subject` |
| `content[]` with `text/plain` and `text/html` | `text` and `html` |
| `template_id` | `template`, a template ID or alias |
| `personalizations[].dynamic_template_data` | `variables` |
| `attachments[]` with `content`, `filename`, `type`, `content_id` | `attachments[]` with `content`, `filename`, `content_type`, `content_id`, or a `url` instead of `content` |
| `headers` | `headers` |
| `custom_args`, `categories` | `meta`, an object of string values returned in webhook events |
| `send_at` (Unix time) | `scheduled_at`, which accepts the same Unix time, ISO 8601 or plain English |
| `tracking_settings.open_tracking` and `click_tracking` | `tracking: { "loads": true, "clicks": true }` |
| `asm` (unsubscribe groups) | Not available |
| `202 Accepted` with an `X-Message-Id` header | `200` with a JSON body: `id`, `status: "accepted"`, and `ids` with one ID per recipient |

Each recipient in an Emailit request becomes its own email with its own ID. To send different variables to different people, which SendGrid does with several `personalizations`, send one request per recipient. Add an `Idempotency-Key` header so retries are safe. See [Send an email](/docs/email-api/send-email/).

## Switch SMTP settings

| Setting | SendGrid | Emailit |
| --- | --- | --- |
| Host | `smtp.sendgrid.net` | `smtp.emailit.com` |
| Port | `587`, `465`, `2525` or `25` | `587` (STARTTLS), `465` (TLS), `2525`, `2587` or `25` |
| Username | `apikey` | `emailit` |
| Password | Your SendGrid API key | Your Emailit API key |

Emailit doesn't read the `X-SMTPAPI` header. Remove it, and set tracking on the domain instead. See [SMTP settings](/docs/smtp/settings/).

## Map webhook events

| SendGrid event | Emailit event |
| --- | --- |
| `processed` | `email.accepted` (API only) |
| `deferred` | `email.attempted` |
| `delivered` | `email.delivered` |
| `bounce` | `email.bounced` |
| `dropped` | `email.suppressed` when the recipient is on the suppression list |
| `open` | `email.loaded` |
| `click` | `email.clicked` |
| `spamreport` | `email.complained` |
| `unsubscribe`, `group_unsubscribe` | `email.unsubscribed`, for campaign email only |
| Inbound Parse POST | `email.received`, then fetch the content with [`GET /emails/{id}`](/docs/api-reference/emails/get/) |

Like SendGrid, Emailit posts a JSON array of events. The fields differ:

- The event name is in `type`, and the email is in `data.object`. Use `data.object.id` (the `em_` ID from the send response) instead of `sg_message_id`, and `data.object.to` instead of `email`.
- Your `meta` values come back in `data.object.meta`.
- Emailit signs requests with HMAC-SHA256 instead of SendGrid's ECDSA public key. Verify `X-Emailit-Signature` with your `whsec_` secret. See [Request signature](/docs/webhooks/request-signature/).

```javascript
for (const event of req.body) {
  const email = event.data.object;
  if (event.type === 'email.bounced') markBounced(email.to, email.id);
  if (event.type === 'email.complained') unsubscribe(email.to);
}
```

## Move suppressions

1. In SendGrid, export your **Bounces**, **Spam Reports**, **Invalid Emails** and **Global Unsubscribes**, from the suppression pages or with the `/v3/suppression/*` API endpoints. Blocks are usually temporary, so you can leave them out.

2. Build one CSV with the columns `email,type,reason`:

```csv
email,type,reason
old-address@example.com,recipient,sendgrid bounce
angry@example.com,recipient,sendgrid spam report
```

   Use the type `recipient` for addresses that must never receive email. It blocks API, SMTP and campaign sends. The types `bounce`, `complaint` and `unsubscribe` only stop campaigns.

3. In **Email API → Suppressions**, select **Import** and upload the file. Each file can have up to 10,000 rows and can be at most 8 MB, so split larger lists. Duplicates are skipped.

For group unsubscribes from marketing email, import those people as contacts with **unsubscribed** set, rather than suppressing them from all email. See [Manage suppressions](/docs/suppressions/manage/).

## Move templates

Export each dynamic template's HTML from SendGrid, then import it in **Email Marketing → Templates** or create it with the [Templates API](/docs/api-reference/templates/create/). Give each template an alias, such as `receipt`, and send it with `"template": "receipt"`.

Both use double curly braces, but Temple is smaller than Handlebars:

| SendGrid (Handlebars) | Emailit (Temple) |
| --- | --- |
| `{{first_name}}` | `{{first_name}}` |
| `{{{html_block}}}` | `{{html_block}}`. Temple never escapes HTML, so escape user input yourself. |
| `{{insert name "default=there"}}` | `{{name\|"there"}}` |
| `{{#if plan}}…{{else}}…{{/if}}` | The same |
| `{{#each items}}…{{/each}}` | Not supported. Render the list in your code and pass it as one variable. |
| `{{#equals plan "pro"}}…{{/equals}}` | Not supported. Pass a boolean such as `is_pro` and use `{{#if is_pro}}`. |

See [Temple](/docs/templates/temple/) and [Import and export templates](/docs/templates/import-export/).

## Change DNS

Add your domain in **Email API → Domains** and publish the Emailit records. They use their own names (`emailit._domainkey`, `emailit.<domain>`, and optionally `go` and `inbound`), so they don't conflict with SendGrid's domain authentication or link branding CNAMEs. Keep your DMARC record. After the cutover, remove the SendGrid CNAMEs. See [DNS records](/docs/domains/dns-records/).

If you used Inbound Parse, point the MX record of your parse hostname at Emailit instead. To keep the same hostname, such as `parse.acme.com`, set the domain's `inbound_key` to `parse` with the API. See [Set up inbound](/docs/inbound/set-up/).

## Next steps

- [Go-live checklist](/docs/get-started/go-live/)
- [Set up webhooks](/docs/webhooks/set-up/)
- [Priority migration](/docs/programs/priority-migration/): let Emailit engineers do the move with you

---
Source: https://emailit.com/docs/migrate/sendgrid/
