Developer guideMigration
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.
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 and use this guide to translate each part.
The send call#
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" }]
}'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 |
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:
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 or use the visual editor 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).
Audiences, contacts and topics#
- Export each audience as CSV and import into Couryo, with the consent confirmation. Whoever had unsubscribed: add them as marketing suppressions first (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. - Broadcasts become campaigns, 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.
- Publish Couryo's records and wait for the domain to be verified.
- Keep the DMARC record you already have; to get the reports in the deliverability agent, add
mailto:reports@dmarc.couryo.comtorua. - Once all traffic is on Couryo and 7 days have passed, delete
resend._domainkeyand the records of thesendsubdomain. Do not touch the MX or the SPF of your mailboxes.