---
title: "Switch from SendGrid"
description: "Keep the SendGrid SDK and send through Nextmail. Change its base URL and its key, and your sending code stays as it is."
url: "https://nextmail.sh/docs/knowledge/developers/switch-from-sendgrid"
area: "Knowledge base"
category: "Developers"
status: "live"
---

# Switch from SendGrid

Keep the SendGrid SDK and send through Nextmail. Change its base URL and its key, and your sending code stays as it is.

## What to change

Point the SDK at https://api.nextmail.sh/compat/sendgrid and give it an API key from Nextmail with the `emails:send` permission in place of the SendGrid key. It travels in the same `Authorization: Bearer` header. Your calls that send mail stay as they are, and the From address decides whether a message may go out, as it does everywhere in Nextmail.

**Point the SendGrid SDK at Nextmail**

_Node.js_

```nodejs
import sgMail from '@sendgrid/mail'
import client from '@sendgrid/client'

client.setApiKey(process.env.NEXTMAIL_API_KEY)
client.setDefaultRequest('baseUrl', 'https://api.nextmail.sh/compat/sendgrid')
sgMail.setClient(client)

await sgMail.send({
  from: 'billing@acme.com',
  to: 'ada@example.com',
  subject: 'Your invoice',
  html: '<p>Your invoice is attached.</p>',
})
```

_Python_

```python
import os
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail

sg = SendGridAPIClient(
    api_key=os.environ["NEXTMAIL_API_KEY"],
    host="https://api.nextmail.sh/compat/sendgrid",
)
sg.send(Mail(
    from_email="billing@acme.com",
    to_emails="ada@example.com",
    subject="Your invoice",
    html_content="<p>Your invoice is attached.</p>",
))
```

_Ruby_

```ruby
require 'sendgrid-ruby'
include SendGrid

sg = SendGrid::API.new(
  api_key: ENV['NEXTMAIL_API_KEY'],
  host: 'https://api.nextmail.sh/compat/sendgrid'
)
mail = Mail.new(
  Email.new(email: 'billing@acme.com'),
  'Your invoice',
  Email.new(email: 'ada@example.com'),
  Content.new(type: 'text/html', value: '<p>Your invoice is attached.</p>')
)
sg.client.mail._('send').post(request_body: mail.to_json)
```

_PHP_

```php
<?php

use SendGrid\Mail\Mail;

$email = new Mail();
$email->setFrom('billing@acme.com');
$email->addTo('ada@example.com');
$email->setSubject('Your invoice');
$email->addContent('text/html', '<p>Your invoice is attached.</p>');

$sendgrid = new SendGrid(getenv('NEXTMAIL_API_KEY'), [
    'host' => 'https://api.nextmail.sh/compat/sendgrid',
]);
$sendgrid->send($email);
```

_curl_

```bash
curl -X POST https://api.nextmail.sh/compat/sendgrid/v3/mail/send \
  -H "Authorization: Bearer $NEXTMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "personalizations": [{ "to": [{ "email": "ada@example.com" }] }],
    "from": { "email": "billing@acme.com" },
    "subject": "Your invoice",
    "content": [{ "type": "text/html", "value": "<p>Your invoice is attached.</p>" }]
  }'
```

> In Node, set the key on the client first, then the base URL, then hand the client to the mail package. Do not call `sgMail.setApiKey` after that, because it puts the base URL back to SendGrid. The SDK warns that the key does not start with `SG.`, which is harmless. In Python, Ruby and PHP, give the host without a trailing slash.

## What maps to what

`POST /v3/mail/send` is the endpoint served. Each entry in `personalizations` becomes its own Nextmail message with its own id, so one request sends at most 100 messages.

| SendGrid | In Nextmail |
| --- | --- |
| `from` | The sender, with its name. A personalization can name its own `from`. |
| `personalizations` | One message each. Its `to`, `cc` and `bcc` hold up to 50 recipients between them, and its `subject`, `headers`, `custom_args`, `send_at` and `substitutions` apply to that message alone. |
| `subject` | The subject, unless a personalization sets its own. |
| `content` | `text/plain` becomes the text body and `text/html` the HTML body. `text/x-amp-html` is left out, because the HTML body still carries the message. |
| `attachments` | Files, 20 at most and 10 MB in all. An inline image whose `content_id` the HTML uses as `cid:` is embedded where it appears. Any other inline file arrives as an ordinary attachment. |
| `reply_to` | The reply-to address. `reply_to_list` works too while it holds one address. |
| `headers` | Custom headers: `X-*`, `List-*`, Reply-To, Precedence, Auto-Submitted, Importance, Priority and Feedback-ID. A personalization adds its own. |
| `categories` | Tags named `category`, `category_2` and so on, each holding one category. |
| `custom_args` | Tags with the same names and values. A personalization’s values win. |
| `send_at` | A scheduled send, up to a year ahead. A time already past sends at once. |
| `substitutions` | Each key is replaced by its value in the subject, the text body and the HTML body of that message. |
| `template_id` | The id (`tpl_...`) or slug of a template in Nextmail, filled in from `dynamic_template_data`. |
| `tracking_settings` | `open_tracking.enable` and `click_tracking.enable` turn open and click tracking on or off for the message. |
| `mail_settings` | `sandbox_mode.enable` checks the request, the sender and the template, then answers 200 without sending anything. |

> A message carries at most 10 tags, counting categories and custom_args together. A request that needs more is refused rather than trimmed, so nothing you sent goes missing without a word.

## What is refused, and why

- A SendGrid template id in `template_id`, such as `d-…`. Templates stay at SendGrid, so recreate the template in Nextmail and send its id or slug.
- `content` next to `template_id`, because a template in Nextmail supplies the whole body. `substitutions` with a template for the same reason: pass the values in `dynamic_template_data`.
- More than one reply-to address, `reply_to` and `reply_to_list` together, and content types other than text and HTML. Send a calendar invitation as an `.ics` attachment.
- `mail_settings.footer` switched on, and `sections`, because Nextmail does not write text into your message.
- More than 10 tags, a tag name other than letters, digits, `_` and `-`, a header outside the list above, and more than 100 personalizations in one request.

`asm`, `batch_id`, `ip_pool_name`, the bypass settings in `mail_settings`, `subscription_tracking`, `ganalytics`, `click_tracking.enable_text` and `open_tracking.substitution_tag` are accepted and change nothing. Addresses on the workspace’s suppression list are always skipped, whatever a bypass setting says.

## Responses and errors

- A send answers 202 with an empty body and the Nextmail message id in `X-Message-Id`, the id that `GET /emails/{id}` and webhooks use. With several personalizations it holds the first message’s id. An `Idempotency-Key` header works as it does on the rest of the API.
- Errors come back as `errors`, a list of `message`, `field` and `help`: 400 for a request that cannot be sent, 401 for a missing or unknown key, 403 for a key without `emails:send`, or a From address the key may not use or whose domain cannot send yet, 413 for a body over 30 MB or attachments over 5 MB, and 429 when the workspace has used up its sending allowance.
- When a personalization fails after earlier ones were accepted, the error names the messages already sent, so a retry can leave them out.

## Bring your account across

Migrations → From a provider reads a SendGrid account with one API key and brings across what can leave it, so lists and templates are not rebuilt by hand. Running it twice duplicates nothing, and the key is held encrypted only while the import runs.

- Suppressions keep a reason: bounces and invalid addresses arrive as bounces, spam reports as complaints, and unsubscribes and blocks as manual.
- Every Marketing list becomes an audience. Its contacts come from a Marketing export, with their email, first name and last name, each one added as subscribed.
- Dynamic templates arrive with their active version. Legacy templates are not read.
- Each webhook endpoint is registered with the same URL and the nearest matching events, switched off, because deliveries here are signed differently. Account status events have no match and are listed instead.
- Domains come back as a checklist with their records, and API keys by name, because a signing key or a token cannot leave the service that made it. An account on SendGrid’s EU region is found on its own.
