# Migrate from SendGrid

> Guide to move from SendGrid to Couryo. Mapping of Mail Send v3 (personalizations, content, template_id, categories, custom_args, send_at), of the Event Webhook events, of suppressions and SMTP, and the DNS change without stopping your sending.

Source: https://couryo.com/en/docs/migrate-sendgrid

SendGrid and Couryo do the same job with different shapes: a SendGrid send carries `personalizations`; at Couryo each email is a plain object, and many go together in a [batch](https://couryo.com/en/docs/sending.md#batch-sending). Follow the [safe migration order](https://couryo.com/en/docs/migration.md) and use this guide to translate each part.

## The send call

```bash title="SendGrid: POST https://api.sendgrid.com/v3/mail/send"
curl https://api.sendgrid.com/v3/mail/send \
  -H "Authorization: Bearer $SENDGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "personalizations": [{ "to": [{ "email": "ana@example.com", "name": "Ana" }] }],
    "from": { "email": "orders@example.com", "name": "Shop" },
    "subject": "Order 1042 confirmed",
    "content": [
      { "type": "text/plain", "value": "Thanks, Ana!" },
      { "type": "text/html", "value": "<p>Thanks, Ana!</p>" }
    ],
    "categories": ["order"],
    "custom_args": { "order_id": "1042" }
  }'
```

```bash title="Couryo: POST https://api.couryo.com/v1/emails"
curl https://api.couryo.com/v1/emails \
  -H "Authorization: Bearer $COURYO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-confirmation" \
  -d '{
    "from": "Shop <orders@example.com>",
    "to": "Ana <ana@example.com>",
    "subject": "Order 1042 confirmed",
    "text": "Thanks, Ana!",
    "html": "<p>Thanks, Ana!</p>",
    "tags": { "category": "order" },
    "metadata": { "order_id": "1042" }
  }'
```

SendGrid answers `202` with no body and an `X-Message-Id` header. Couryo answers `202 { "id": "msg_...", "status": "queued" }`, only after the email is in the queue.

## Fields

| SendGrid | Couryo | Note |
|---|---|---|
| `from: { email, name }` | `from: "Name <email>"` | a string, on a verified domain |
| `personalizations[].to/cc/bcc` | `to`, `cc`, `bcc` (string or list) | up to 50 recipients per email; each personalization becomes a batch item |
| `reply_to` / `reply_to_list` | `reply_to` | |
| `subject` | `subject` | |
| `content` (`text/plain`, `text/html`) | `text`, `html` | send both: the text part helps delivery |
| `template_id` + `dynamic_template_data` | `template` + `variables` | the template must exist in Couryo (see below) |
| `attachments[]` (`content`, `filename`, `type`, `content_id`) | `attachments[]` (`content`, `filename`, `content_type`, `content_id`) | Base64, up to 20 |
| `headers` | `headers` | platform headers (`From`, `Message-ID`, `List-Unsubscribe`...) are ignored |
| `categories` | `tags` | key and value, e.g. `{ "category": "order" }` |
| `custom_args` | `metadata` | comes back in every webhook |
| `send_at` (Unix, up to 72 h) | `scheduled_at` (ISO 8601, up to 30 days) | [reschedule and cancel](https://couryo.com/en/docs/sending.md#reschedule-or-cancel) by the email id |
| `batch_id` + cancel scheduled sends | `POST /v1/emails/{id}/cancel` | one per email |
| `asm.group_id` (unsubscribe groups) | `stream: "marketing"` + [topics](https://couryo.com/en/docs/contacts.md#topics-and-preferences) | one-click unsubscribe is added to marketing on its own |
| `tracking_settings` | [project tracking](https://couryo.com/en/docs/tracking.md) | on for marketing, off for transactional by default |
| `mail_settings.sandbox_mode` | a `ck_test_` key | captures without delivering, with simulated events |

## Dynamic templates

SendGrid templates use Handlebars. At Couryo, variables are `{{name}}` and `{{order.total}}`, with an optional fallback (`{{name|there}}`); blocks like `{{#if}}` and `{{#each}}` do not exist. Recreate each template under **Templates** (HTML, MJML or the visual editor) and send `template` with the name or the id:

```json title="Body with a template"
{ "from": "Shop <orders@example.com>", "to": "ana@example.com", "template": "order-confirmed", "variables": { "name": "Ana", "order": { "number": "1042" } } }
```

Product lists and conditional parts: build that piece of HTML in your code and pass it as a variable, or use one template per case.

## Event Webhook events

| SendGrid | Couryo |
|---|---|
| `processed` | `accepted` and `queued` |
| `delivered` | `delivered` |
| `deferred` | `deferred` |
| `bounce` (and `blocked`) | `bounced` |
| `dropped` | `suppressed` (suppressed address) or `failed` (stopped before sending) |
| `spamreport` | `complained` |
| `open` | `opened` (with `machine: true` when the mail program opened it, not the person) |
| `click` | `clicked` (with the `url`) |
| `unsubscribe`, `group_unsubscribe` | no event: the address goes to the marketing suppression list (`GET /v1/suppressions?reason=unsubscribe`) |

The Event Webhook sends many events in an array and signs with ECDSA. Couryo sends **one event per call**, following [Standard Webhooks](https://couryo.com/en/docs/webhooks.md) (HMAC-SHA256 with the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers), with retries for up to 3 days.

## Suppressions and contacts

- Export from SendGrid (Suppressions) the **Bounces**, **Spam Reports**, **Invalid Emails** and permanent **Blocks** lists, and add them to Couryo with `stream: "all"`. **Unsubscribes** and **Group Unsubscribes** go with `stream: "marketing"`. The loop is in [copy the suppressions](https://couryo.com/en/docs/migration.md#copy-the-suppressions).
- Marketing Campaigns contacts: export as CSV and [import into Couryo](https://couryo.com/en/docs/contacts.md#import-from-csv), with the consent confirmation. Lists become tags or [topics](https://couryo.com/en/docs/contacts.md#topics-and-preferences).

## SMTP

If you use the SendGrid relay (`smtp.sendgrid.net`, username `apikey`), change only the credentials: server `smtp.couryo.com`, port `587`, username `couryo` and the Couryo key as the password. The `X-SMTPAPI` header does not exist at Couryo: use `X-Couryo-Tags` and `X-Couryo-Stream` ([SMTP](https://couryo.com/en/docs/smtp.md#options-by-header)).

## DNS

SendGrid uses its own records (the `em…`, `s1._domainkey` and `s2._domainkey` CNAMEs, plus the link branding ones). They do not conflict with Couryo's (`couryo1._domainkey`, `couryo2._domainkey` and `bounces`), so both stay published during the migration.

1. Publish Couryo's records and wait for the domain to be verified.
2. Keep the DMARC record you already have: Couryo respects it. To get the reports in the [deliverability agent](https://couryo.com/en/docs/deliverability.md), add `mailto:reports@dmarc.couryo.com` to `rua`.
3. Once all traffic is on Couryo and 7 days have passed, delete the SendGrid CNAMEs. Do not touch the MX or the SPF of your mailboxes.
