Skip to content
couryo

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.

View as Markdown

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#

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

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

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