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

# Switch from SendGrid

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

## What to change

Point the SDK at https://api.openemail.uk/compat/sendgrid and give it an OpenEmail API key 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 OpenEmail.

**Point the SendGrid SDK at OpenEmail**

_Node.js_

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

client.setApiKey(process.env.OPENEMAIL_API_KEY)
client.setDefaultRequest('baseUrl', 'https://api.openemail.uk/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["OPENEMAIL_API_KEY"],
    host="https://api.openemail.uk/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['OPENEMAIL_API_KEY'],
  host: 'https://api.openemail.uk/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('OPENEMAIL_API_KEY'), [
    'host' => 'https://api.openemail.uk/compat/sendgrid',
]);
$sendgrid->send($email);
```

_curl_

```bash
curl -X POST https://api.openemail.uk/compat/sendgrid/v3/mail/send \
  -H "Authorization: Bearer $OPENEMAIL_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 OpenEmail message with its own id, so one request sends at most 100 messages.

| SendGrid | In OpenEmail |
| --- | --- |
| `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 5 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 an OpenEmail template, 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 OpenEmail and send its id or slug.
- `content` next to `template_id`, because an OpenEmail template 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 OpenEmail 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 OpenEmail 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.
