getreadystack's picture
Upload index.html with huggingface_hub
9a416eb verified
Raw History Blame Contribute Delete
42.4 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Resend SendGrid Migration — check your code for free</title>
<meta name="description" content="This free tool flags SendGrid calls, fields and webhook events without a direct Resend equivalent and shows what to write instead.">
<style>
:root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512}
.wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px}
textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box}
.bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer}
input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px}
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65}
.sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600}
.note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa}
</style>
<script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
<!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/resend-migration-check"><style data-yj-mfit="1">#out{max-width:100%}#out td,#out th{overflow-wrap:anywhere}#out pre,#out code{white-space:pre-wrap;overflow-wrap:anywhere}@media (max-width:640px){table{display:block;max-width:100%;overflow-x:auto}pre{max-width:100%;overflow-x:auto}code,a{overflow-wrap:anywhere}img,svg,video,canvas,iframe{max-width:100%}textarea,input,select{max-width:100%;box-sizing:border-box}.yj-lgbar{position:static!important;justify-content:flex-end;margin:0 0 10px}#out table:not(:has(tr>:nth-child(4))){display:table;table-layout:fixed;width:100%}#out table:has(tr>:nth-child(4)),#out table:has(tr>:nth-child(4)) tbody,#out table:has(tr>:nth-child(4)) tr,#out table:has(tr>:nth-child(4)) td{display:block;width:auto;overflow:visible}#out table:has(tr>:nth-child(4)) thead{display:none}#out table:has(tr>:nth-child(4)) tr{padding:10px 0;border-bottom:1px solid rgba(128,128,128,.28)}#out table:has(tr>:nth-child(4)) td{border:0;padding:1px 0}#out table:has(tr>:nth-child(4)) td:nth-child(-n+3){display:inline-block;margin-right:10px;font-size:12px;opacity:.78}}</style>
</head>
<body>
<div class="wrap">
<h1>Resend SendGrid Migration — check your code for free</h1>
<p class="lede">Moving from Twilio SendGrid to Resend? Flags every @sendgrid/mail and sendgrid Python call, field and webhook event that has no 1:1 Resend equivalent, with the Resend line to write instead. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false">// order-mail.js: order receipts and the bounce webhook, still on Twilio SendGrid
const sgMail = require('@sendgrid/mail');
const express = require('express');
const key = process.env.SENDGRID_API_KEY;
sgMail.setApiKey(key);
async function sendReceipt(order) {
const msg = {
to: order.email,
from: 'orders@shop.example',
subject: `Receipt for order ${order.id}`,
templateId: 'd-2c214ac919e84170b21855cc129b4a5f',
sendAt: Math.floor(Date.now() / 1000) + 600,
asm: { groupId: 21 },
mailSettings: { sandboxMode: { enable: process.env.NODE_ENV !== 'production' } },
};
return sgMail.send(msg);
}
const app = express();
app.post('/hooks/mail', express.json(), (req, res) =&gt; {
for (const e of req.body) {
if (e.event === 'bounce') suppress(e.email);
}
res.sendStatus(200);
});
function suppress(email) { /* add to our own do-not-mail list */ }
module.exports = { sendReceipt, app };</textarea>
<div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div>
<div id="out" aria-live="polite"></div>
<div id="paid"><strong>Free here:</strong> Checks the open file and names every SendGrid line that breaks on Resend, with the Resend line to write instead. <strong>Full version:</strong> Scans the whole workspace and exports one migration plan: every SendGrid use mapped to its Resend replacement, file by file. <a href="https://getreadystack.com/api/buy/cl/polar_cl_xOhImPHSteHX7SqaXxIkCtTMR6Nmb2Cvb947U0otllF" data-polar-checkout data-polar-checkout-theme="light">Get the full version — $29</a> · $29 once · one licence key per person or team seat. Twilio SendGrid's paid Essentials plan starts at $19.95 a month.</div>
<script defer data-auto-init src="https://cdn.jsdelivr.net/npm/@polar-sh/checkout@latest/dist/embed.global.js"></script>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var RMC_RULES = [
{
"id": "sg-import-node",
"sev": "error",
"re": "require\\(\\s*['\"]@sendgrid/|from\\s+['\"]@sendgrid/",
"msg": "@sendgrid/* package. Resend: npm i resend, then import { Resend } from 'resend'.",
"gate": "sendgrid|sgMail|X-Twilio-Email|EventWebhook"
},
{
"id": "sg-import-python",
"sev": "error",
"re": "^\\s*(from\\s+sendgrid(\\.[\\w.]+)?\\s+import|import\\s+sendgrid)\\b",
"msg": "sendgrid Python package. Resend: pip install resend, import resend, set resend.api_key."
},
{
"id": "sg-set-api-key",
"sev": "error",
"re": "\\.setApiKey\\s*\\(|SendGridAPIClient\\s*\\(",
"msg": "SendGrid client setup. Resend: const resend = new Resend(process.env.RESEND_API_KEY)."
},
{
"id": "sg-env-key",
"sev": "error",
"re": "SENDGRID_API_KEY",
"msg": "SENDGRID_API_KEY holds an SG. key that Resend rejects. Resend: RESEND_API_KEY with a re_ key."
},
{
"id": "sg-raw-endpoint",
"sev": "error",
"re": "api\\.sendgrid\\.com/v3/mail/send",
"msg": "Raw SendGrid v3 endpoint. Resend: POST https://api.resend.com/emails with a flat {from, to, subject, html} body."
},
{
"id": "sg-send-multiple",
"sev": "error",
"re": "\\.sendMultiple\\s*\\(",
"msg": "sendMultiple does not exist on Resend. Resend: resend.batch.send([...]) with up to 100 emails per call."
},
{
"id": "sg-personalizations",
"sev": "error",
"re": "\\bpersonalizations\\b|\\bPersonalization\\s*\\(",
"msg": "personalizations has no Resend field. Resend: one email object per recipient in resend.batch.send (up to 100 per call, up to 50 addresses in to)."
},
{
"id": "sg-dynamic-template",
"sev": "error",
"re": "\\btemplate_?[iI]d\\b|['\"]d-[0-9a-f]{32}['\"]",
"msg": "SendGrid dynamic template id (d-...) exists only in your SendGrid account. Resend: rebuild the template on Resend or send rendered html/react."
},
{
"id": "sg-template-data",
"sev": "warn",
"re": "dynamicTemplateData|dynamic_template_data",
"msg": "Handlebars template data for a SendGrid template. Resend: render the values into html (or react) before resend.emails.send."
},
{
"id": "sg-send-at",
"sev": "error",
"re": "\\bsendAt\\b|\\bsend_at\\b",
"msg": "sendAt (Unix seconds) is SendGrid-only. Resend reads scheduledAt, an ISO 8601 string, not sendAt."
},
{
"id": "sg-send-at-past",
"type": "ts_past",
"sev": "warn",
"re": "\\b(?:sendAt|send_at)\\b\\s*[:=]\\s*(\\d{10})\\b",
"msg": "Hard-coded send time {ts} is already before {today}. Build scheduledAt from the send moment as an ISO 8601 string."
},
{
"id": "sg-categories",
"sev": "warn",
"re": "\\bcategories\\s*[:=]|\\bcustomArgs\\b|\\bcustom_args\\b|\\bCategory\\s*\\(",
"msg": "categories / customArgs are SendGrid-only. Resend: tags: [{ name, value }] (ASCII letters, numbers, _ and -)."
},
{
"id": "sg-asm",
"sev": "error",
"re": "\\basm\\s*[:=]|\\bAsm\\s*\\(|\\bgroupId\\b|\\bgroup_id\\b",
"msg": "Unsubscribe group (asm) has no field in resend.emails.send; the group is dropped. Keep your own suppression list before sending."
},
{
"id": "sg-tracking",
"sev": "warn",
"re": "trackingSettings|tracking_settings|TrackingSettings",
"msg": "Per-message tracking settings. Resend: open and click tracking are switched per domain, not per email; this block is dropped."
},
{
"id": "sg-sandbox",
"sev": "error",
"re": "sandboxMode|sandbox_mode|SandBoxMode",
"msg": "sandboxMode has no Resend field, so the email is really sent. Resend: send tests to delivered@resend.dev instead."
},
{
"id": "sg-ip-pool",
"sev": "warn",
"re": "ipPoolName|ip_pool_name",
"msg": "ipPoolName has no field in resend.emails.send; the pool choice is dropped."
},
{
"id": "sg-webhook-signature",
"sev": "error",
"re": "X-Twilio-Email-Event-Webhook-(Signature|Timestamp)|\\bEventWebhook\\b|verifySignature",
"msg": "SendGrid Event Webhook signature check. Resend signs webhooks with Svix: verify svix-id, svix-timestamp and svix-signature headers."
},
{
"id": "sg-webhook-event",
"type": "event_map",
"sev": "error",
"re": "(===?|\\bcase|\\bin)\\s*\\(?\\s*['\"](processed|dropped|delivered|deferred|bounce|open|click|spamreport|unsubscribe|group_unsubscribe|group_resubscribe)['\"]",
"msg": "SendGrid event '{ev}' never arrives from Resend: the field is type, not event. Resend: {to}.",
"map": {
"processed": "type === 'email.sent'",
"delivered": "type === 'email.delivered'",
"deferred": "type === 'email.delivery_delayed'",
"bounce": "type === 'email.bounced'",
"open": "type === 'email.opened'",
"click": "type === 'email.clicked'",
"spamreport": "type === 'email.complained'",
"dropped": "no email.* event with this name; handle email.bounced",
"unsubscribe": "no email.* event with this name; keep your own unsubscribe list",
"group_unsubscribe": "no email.* event with this name; keep your own unsubscribe list",
"group_resubscribe": "no email.* event with this name; keep your own unsubscribe list"
}
}
];</script>
<script>// resend-migration-check engine: SendGrid lines that break on Resend. Same file runs in VS Code and the browser.
(function () {
const RULES = (typeof module !== 'undefined') ? require('./rules.json') : window.RMC_RULES;
const GATE = new RegExp(RULES[0].gate, 'i'); // files that never mention SendGrid stay silent
const COMPILED = RULES.map(r => Object.assign({}, r, { rx: new RegExp(r.re) }));
function day(s) {
const d = new Date(String(s || '').slice(0, 10) + 'T00:00:00Z');
return isNaN(d) ? null : d;
}
function check(text, opts) {
opts = opts || {};
const today = day(opts.today) || day(new Date().toISOString());
const todayIso = today.toISOString().slice(0, 10);
const findings = [];
const src = String(text || '');
if (!GATE.test(src)) return { findings, today: todayIso };
const lines = src.split(/\r?\n/);
lines.forEach((line, i) => {
if (/^\s*(\/\/|#|\*|\/\*)/.test(line)) return;
for (const r of COMPILED) {
const m = line.match(r.rx);
if (!m) continue;
let msg = r.msg;
if (r.type === 'ts_past') {
const at = new Date(Number(m[1]) * 1000);
if (!(at < today)) continue;
msg = msg.replace('{ts}', at.toISOString().slice(0, 10)).replace('{today}', todayIso);
} else if (r.type === 'event_map') {
msg = msg.replace('{ev}', m[2]).replace('{to}', r.map[m[2]]);
}
findings.push({ check: r.id, sev: r.sev, msg, line: i + 1 });
}
});
return { findings, today: todayIso };
}
const api = { engine: { check }, RULES, RULE_COUNT: RULES.length };
if (typeof module !== 'undefined') module.exports = api;
if (typeof window !== 'undefined') window.RMCENGINE = api;
})();
</script>
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
'use strict';
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
function summaryLine(res) {
const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length;
return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.';
}
function toText(res, label) {
const f = (res && res.findings) || [];
const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)];
f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); });
return lines.join('\n');
}
function toHtml(res) {
const f = (res && res.findings) || [];
if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>';
const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join('');
return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
var RMCREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = RMCREPORT;
else window.RMCREPORT = RMCREPORT;
</script>
<script>
(function(){
var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today');
today.value=new Date().toISOString().slice(0,10);
function run(){ try { var res=window.RMCENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.RMCREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } }
go.onclick=run; if (src.value.trim()) run();
})();
</script>
<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://open-vsx.org/extension/readystack/resend-migration-check" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'resend-migration-check',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/team" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get one email when this rule changes</b><div style="opacity:.78;margin:.35rem 0 .8rem">We watch the regulation and vendor sources behind Resend Migration Check: SendGrid code lint every day. When a rule changes, you get a single email with what changed and the updated check. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Notify me</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">You're on the list. One email when it changes.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"resend-migration-check",lang:"en",source:"rule_alert"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);document.addEventListener("click",function(ev){var b=ev.target&&ev.target.closest?ev.target.closest("button,input[type=submit]"):null;if(b&&!b.closest("[data-yj],[data-yj-cta],[data-yj-install],[data-yj-alert],.yj-find,.yj-related,#yj-ask,.yjp"))mv(ev);},true);})();</script></section>
<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the full version <span class="yj-price">$29</span></h2><p class="yj-cta-paid" style="margin:.25rem 0 .6rem;color:#1f2a37">Scans the whole workspace and exports one migration plan: every SendGrid use mapped to its Resend replacement, file by file.</p><p class="yj-cta-pay" style="margin:.25rem 0 1rem;color:#5b6b7c;font-size:.94rem">One payment, one licence key for this tool. The key is shown right after payment.</p><p class="yj-cta-anchor" style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">Twilio SendGrid's paid Essentials plan starts at $19.95 a month now that its free plan is retired; the workspace migration plan here is $29 once.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_xOhImPHSteHX7SqaXxIkCtTMR6Nmb2Cvb947U0otllF" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
<!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script>
<script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script>
<script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div><section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-29">Updated 2026-09-29</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-29","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku405904_result_card.jpg" alt="Resend Migration Check: SendGrid code lint" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>Eight SendGrid lines break on Resend in order-mail.js, a Node.js receipt mailer with a bounce webhook, and a developer moving off Twilio SendGrid gets no error from any of them.</p>
<h3>Migrate from Twilio SendGrid to Resend: the lines that fail silently</h3>
<p>Twilio SendGrid retired its free Email API plan starting May 27, 2025 (Twilio changelog: &quot;Changes coming to SendGrid&#x27;s Free Plan&quot;). New accounts have had a 60-day trial since March 25, 2025, and the paid Essentials plan starts at $19.95 a month. Resend keeps a free tier of 3,000 emails a month and a send call that looks almost the same, so plenty of teams are switching.</p>
<p>The send call is the easy part. <code>sgMail.send(msg)</code> becomes <code>resend.emails.send({...})</code>. The trouble is the fields that have no Resend twin. They do not throw. They are dropped, and the mail behaves differently.</p>
<h3>What the check found in order-mail.js</h3>
<p>Resend Migration Check ran its 18 rules over the sample file and returned eight findings:</p>
<table><thead><tr><th>SendGrid line</th><th>Resend fix</th></tr></thead><tbody><tr><td><code>require(&#x27;@sendgrid/mail&#x27;)</code></td><td><code>require(&#x27;resend&#x27;)</code></td></tr><tr><td><code>SENDGRID_API_KEY</code></td><td><code>RESEND_API_KEY</code></td></tr><tr><td><code>sgMail.setApiKey(key)</code></td><td><code>new Resend(key)</code></td></tr><tr><td><code>templateId: &#x27;d-…&#x27;</code></td><td>rebuild on Resend or send html</td></tr><tr><td><code>sendAt: Unix seconds</code></td><td><code>scheduledAt: ISO string</code></td></tr><tr><td><code>asm: { groupId }</code></td><td>dropped: keep your own list</td></tr><tr><td><code>sandboxMode</code></td><td>really sent: use delivered@resend.dev</td></tr><tr><td><code>e.event === &#x27;bounce&#x27;</code></td><td><code>e.type === &#x27;email.bounced&#x27;</code></td></tr></tbody></table>
<p>The migrated file, with <code>new Resend(process.env.RESEND_API_KEY)</code>, an ISO <code>scheduledAt</code>, <code>tags</code> and a <code>type === &#x27;email.bounced&#x27;</code> branch, returns zero.</p>
<h3>The three that cost the most</h3>
<p><strong>sandboxMode.</strong> In SendGrid, <code>mailSettings.sandboxMode.enable</code> validates a message without delivering it. Resend has no such field. Keep the block after the switch and your staging environment sends real receipts to real customers. Resend&#x27;s answer is test addresses such as <code>delivered@resend.dev</code>.</p>
<p><strong>asm groupId.</strong> SendGrid&#x27;s unsubscribe groups (<code>asm</code>) stop mail to people who opted out of one kind of message. <code>resend.emails.send</code> has no field for it, so the group is ignored and the opted-out address gets the email. You need your own suppression check before sending.</p>
<p><strong>event === &#x27;bounce&#x27;.</strong> SendGrid&#x27;s Event Webhook posts an array of objects with an <code>event</code> field: <code>bounce</code>, <code>spamreport</code>, <code>deferred</code>. Resend posts one object with a <code>type</code> field: <code>email.bounced</code>, <code>email.complained</code>, <code>email.delivery_delayed</code>. An <code>if (e.event === &#x27;bounce&#x27;)</code> branch never runs again, and bounced addresses stay on your list. The signature header changes too: Resend signs with Svix (<code>svix-id</code>, <code>svix-timestamp</code>, <code>svix-signature</code>) instead of <code>X-Twilio-Email-Event-Webhook-Signature</code>.</p>
<h3>Why find-and-replace is not enough</h3>
<p>Find-and-replace swaps <code>@sendgrid/mail</code> for <code>resend</code> and <code>setApiKey</code> for a constructor. It does not know that <code>sandboxMode</code>, <code>asm</code> and the webhook event names have no Resend field. The code still compiles and runs. That is why each finding here names the Resend line, or says plainly that there is none.</p>
<h3>Python too</h3>
<p>The same rules read Python. <code>from sendgrid import SendGridAPIClient</code>, <code>SendGridAPIClient(os.environ[&quot;SENDGRID_API_KEY&quot;])</code> and <code>message.send_at = 1735689600</code> are flagged, and a hard-coded <code>send_at</code> is compared with the check date you set: on 2026-09-29 that timestamp (2025-01-01) is already in the past.</p>
<h3>Try it</h3>
<p>Search &quot;resend migration check&quot; in VS Code or Open VSX and run it on the open file. The free check has no key and no limit. The full version exports a workspace-wide migration plan, file by file.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku405904_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku405904_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does Resend Migration Check do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads JavaScript, TypeScript and Python files that call Twilio SendGrid and flags every line that breaks on Resend: the @sendgrid/mail import, SENDGRID_API_KEY, setApiKey, d- template ids, sendAt, asm unsubscribe groups, sandboxMode, the Event Webhook signature and event names such as &#x27;bounce&#x27;. Each finding shows the Resend line to write instead, with the line number.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is Resend Migration Check for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Node.js and Python developers who send transactional email (receipts, password resets, sign-up mail) through Twilio SendGrid and are moving to Resend, often because SendGrid retired its free Email API plan starting May 27, 2025. It suits a solo developer migrating one app or a team lead checking a repository before the API key is swapped.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just use find-and-replace or a chatbot for the SendGrid to Resend switch?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Find-and-replace swaps the package name and the send call. It does not see that sandboxMode, asm unsubscribe groups and an event === &#x27;bounce&#x27; branch have no Resend field. That code still runs: test mail is really sent, unsubscribe groups are dropped and the bounce handler never fires, so nothing errors while mail goes wrong.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what does the full version add?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">The free check runs on the open file with all 18 rules, no key and no limit, and lists every SendGrid line with its Resend replacement. The full version, $29 once, scans the whole workspace and exports one migration plan that maps every SendGrid use to its Resend replacement file by file, so the whole switch can be planned and reviewed.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does staying on SendGrid cost compared with this check?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Twilio SendGrid&#x27;s paid Essentials plan starts at $19.95 a month now that the free plan is retired; new accounts only get a 60-day trial. Resend keeps a free tier of 3,000 emails a month. The file check here is free, and the workspace migration plan is a one-time $29 licence key per person or team seat.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does Resend Migration Check do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads JavaScript, TypeScript and Python files that call Twilio SendGrid and flags every line that breaks on Resend: the @sendgrid/mail import, SENDGRID_API_KEY, setApiKey, d- template ids, sendAt, asm unsubscribe groups, sandboxMode, the Event Webhook signature and event names such as 'bounce'. Each finding shows the Resend line to write instead, with the line number."}}, {"@type": "Question", "name": "Who is Resend Migration Check for?", "acceptedAnswer": {"@type": "Answer", "text": "Node.js and Python developers who send transactional email (receipts, password resets, sign-up mail) through Twilio SendGrid and are moving to Resend, often because SendGrid retired its free Email API plan starting May 27, 2025. It suits a solo developer migrating one app or a team lead checking a repository before the API key is swapped."}}, {"@type": "Question", "name": "Why not just use find-and-replace or a chatbot for the SendGrid to Resend switch?", "acceptedAnswer": {"@type": "Answer", "text": "Find-and-replace swaps the package name and the send call. It does not see that sandboxMode, asm unsubscribe groups and an event === 'bounce' branch have no Resend field. That code still runs: test mail is really sent, unsubscribe groups are dropped and the bounce handler never fires, so nothing errors while mail goes wrong."}}, {"@type": "Question", "name": "What is free and what does the full version add?", "acceptedAnswer": {"@type": "Answer", "text": "The free check runs on the open file with all 18 rules, no key and no limit, and lists every SendGrid line with its Resend replacement. The full version, $29 once, scans the whole workspace and exports one migration plan that maps every SendGrid use to its Resend replacement file by file, so the whole switch can be planned and reviewed."}}, {"@type": "Question", "name": "What does staying on SendGrid cost compared with this check?", "acceptedAnswer": {"@type": "Answer", "text": "Twilio SendGrid's paid Essentials plan starts at $19.95 a month now that the free plan is retired; new accounts only get a 60-day trial. Resend keeps a free tier of 3,000 emails a month. The file check here is free, and the workspace migration plan is a one-time $29 licence key per person or team seat."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:405904,slug:"resend-migration-check",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/brave-search-api-migration-check"><span class="yj-rel-t">Brave Search API Migration Check — Google Custom Search &amp; Bing</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/bulk-sender-lint"><span class="yj-rel-t">Bulk Sender Lint - the Gmail, Yahoo and Outlook.com rules, checked in your mai</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/python-upgrade-removed-api-lint"><span class="yj-rel-t">Python Upgrade Lint: Removed Stdlib 3.12-3.14</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/docker-image-for-development"><span class="yj-rel-t">Docker Image for Development — Node.js 22 + TypeScript Dev Container with Post</span><span class="yj-rel-p">$19</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/robots-txt-ai-crawler-audit"><span class="yj-rel-t">AI Crawler Rules - robots.txt Audit for AI Search</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/almalinux-migration-lint"><span class="yj-rel-t">AlmaLinux Migration Lint — CentOS 7/8</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body>
</html>