Developer guideMigration
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.
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. Follow the safe migration order and use this guide to translate each part.
The send call#
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" }
}'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 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 |
one-click unsubscribe is added to marketing on its own |
tracking_settings |
project tracking | 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:
{ "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 (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 withstream: "marketing". The loop is in copy the suppressions. - Marketing Campaigns contacts: export as CSV and import into Couryo, with the consent confirmation. Lists become tags or topics.
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).
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.
- Publish Couryo's records and wait for the domain to be verified.
- Keep the DMARC record you already have: Couryo respects it. 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 the SendGrid CNAMEs. Do not touch the MX or the SPF of your mailboxes.