Skip to content
couryo

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.

View as Markdown

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#

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" }]
  }'
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
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:

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 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.

  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, 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.