# Migrate from Resend

> Guide to move from Resend to Couryo. The send call is almost the same (from, to, subject, html, text, tags, scheduled_at, Idempotency-Key, batch of 100), with the mapping of events, webhooks, audiences, React Email and the DNS change.

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

Coming from Resend, you will recognize almost everything: the send body, the batch of 100, the `Idempotency-Key`, scheduling with cancel and signed webhooks. The differences are in the details below. 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="Resend: POST https://api.resend.com/emails"
curl https://api.resend.com/emails \
  -H "Authorization: Bearer $RESEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Shop <orders@example.com>",
    "to": ["ana@example.com"],
    "subject": "Order 1042 confirmed",
    "html": "<p>Thanks, Ana!</p>",
    "tags": [{ "name": "category", "value": "order" }]
  }'
```

```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@example.com"],
    "subject": "Order 1042 confirmed",
    "html": "<p>Thanks, Ana!</p>",
    "text": "Thanks, Ana!",
    "tags": { "category": "order" }
  }'
```

Resend answers `{ "id": "..." }`; Couryo answers `202 { "id": "msg_...", "status": "queued" }`, only after the email is in the queue.

## Fields

| Resend | Couryo | Note |
|---|---|---|
| `from`, `to`, `cc`, `bcc`, `reply_to`, `subject`, `html`, `text` | the same names | up to 50 recipients across `to`, `cc` and `bcc` |
| `react` (SDK) | `html` and `text` | render with React Email's `render` before sending (see below) |
| `tags: [{ name, value }]` | `tags: { name: value }` | an object, not a list |
| `headers` | `headers` | platform headers are ignored |
| `attachments[]` (`filename`, `content`, `path`, `content_type`) | `attachments[]` (`filename`, `content` in Base64, `content_type`, `content_id`) | no `path`: download the file and send its content |
| `scheduled_at` (ISO or text like "in 1 hour") | `scheduled_at` (ISO 8601 with a time zone, up to 30 days) | ISO dates only |
| `PATCH /emails/{id}` and `POST /emails/{id}/cancel` | `PATCH /v1/emails/{id}` and `POST /v1/emails/{id}/cancel` | the [same design](https://couryo.com/en/docs/sending.md#reschedule-or-cancel) |
| `POST /emails/batch` (up to 100) | `POST /v1/emails/batch` (up to 100) | Couryo validates each item on its own and returns one result per item |
| `Idempotency-Key` | `Idempotency-Key` | 24 hours; the same body returns the same answer, a different body `409` |
| (no equivalent) | `metadata` | your data, back in every webhook, like `{ "order_id": "1042" }` |
| (no equivalent) | `stream: "marketing"` | one-click unsubscribe included and suppression kept apart from transactional |

## React Email

Couryo takes ready HTML. With React Email, render it first:

```ts title="Node.js"
import { render } from "@react-email/render";
import { OrderConfirmed } from "./emails/order-confirmed";

const html = await render(<OrderConfirmed name="Ana" />);
const text = await render(<OrderConfirmed name="Ana" />, { plainText: true });

await fetch("https://api.couryo.com/v1/emails", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.COURYO_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": "order-1042" },
  body: JSON.stringify({ from: "Shop <orders@example.com>", to: "ana@example.com", subject: "Order 1042 confirmed", html, text }),
});
```

For emails your team edits without a deploy, create [saved templates](https://couryo.com/en/docs/templates.md) or use the [visual editor](https://couryo.com/en/docs/template-editor.md) and send `template` with `variables`.

## Events and webhooks

| Resend | Couryo |
|---|---|
| `email.sent` | `sent` |
| `email.delivered` | `delivered` |
| `email.delivery_delayed` | `deferred` |
| `email.bounced` | `bounced` |
| `email.complained` | `complained` |
| `email.opened` | `opened` |
| `email.clicked` | `clicked` |
| `email.failed` | `failed` |

Both sign following Standard Webhooks: Couryo uses the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers (HMAC-SHA256 with the `whsec_...` secret). If your receiver checks the signature with a library of the standard, change the secret and the header prefix. The body `type` comes without the `email.` prefix (for example, `"type": "bounced"`) and `data` brings `email_id`, `recipient`, the SMTP code, the raw answer of the receiving server and the plain-language explanation, in the account language ([webhooks](https://couryo.com/en/docs/webhooks.md)).

## Audiences, contacts and topics

- Export each audience as CSV and [import into Couryo](https://couryo.com/en/docs/contacts.md#import-from-csv), with the consent confirmation. Whoever had unsubscribed: add them as marketing suppressions first ([copy the suppressions](https://couryo.com/en/docs/migration.md#copy-the-suppressions)).
- Resend topics exist at Couryo with the same name: create them under **Contacts > Topics** or with `POST /v1/topics`. The marketing footer leads to the [preferences page](https://couryo.com/en/docs/contacts.md#topics-and-preferences).
- Broadcasts become [campaigns](https://couryo.com/en/docs/campaigns.md), with an audience by tag, attribute or topic.

## DNS

Resend publishes the DKIM key on its own record (`resend._domainkey`) and the return path on the `send` subdomain. None of them conflicts 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; 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 `resend._domainkey` and the records of the `send` subdomain. Do not touch the MX or the SPF of your mailboxes.
