Migration guides

Migrate from SendGrid to Loops

Swap the packages, move your templates into Loops, keep your function signatures, and cut over without hurting deliverability. This guide follows a real migration an unaided coding agent completed from public docs.

Should you do this?

Migrate if you’re a SaaS sending transactional email from code and marketing email from or alongside SendGrid, and you want both in one platform with templates your team can edit without deploys. If all you need is a raw SMTP pipe at the lowest possible cost, SES-style infrastructure is the better fit, and this guide won’t pretend otherwise.

Plan for one structural change: with SendGrid your email HTML lives in code; Loops has no raw HTML at send time. Subject, body, and design move into published Loops templates, and code passes only the dynamic values as dataVariables. Copy changes stop requiring deploys; the tradeoff is keeping variable names in sync between template and code.

Give this to your agent



Swap the packages

npm uninstall @sendgrid/mail @sendgrid/client
npm

The package is loops. Not loops-so, not loops-js, not @loops/sdk.

API mapping

  • sgMail.send({ to, from, subject, html }) becomes loops.sendTransactionalEmail({ transactionalId, email, dataVariables }). Content lives in a published dashboard template; dynamic values go in dataVariables.

  • Per-send from addresses become your verified sending domain plus per-template From fields. Data variables work inside From and Subject fields if you need dynamism.

  • PUT /v3/marketing/contacts becomes loops.updateContact({ email, properties, mailingLists }). Both upsert; setting a list id to false removes the contact from that list.

  • Error handling carries over: the SDK throws APIError with statusCode and json, so existing try/catch behavior keeps working.

Before and after

// Before (SendGrid)
await sgMail.send({
  to,
  from: { email: "[email protected]", name: "Acme" },
  subject: "Reset your password",
  html: `<p><a href="${resetUrl}">Reset password</a></p>`,
});

// After (Loops) with a published "Password reset" template
// containing a {resetUrl} data variable
await loops.sendTransactionalEmail({
  transactionalId: process.env.LOOPS_TRANSACTIONAL_ID_PASSWORD_RESET!,
  email: to,
  dataVariables: { resetUrl },
});
// Before (SendGrid marketing contacts)
await sgClient.request({
  url: "/v3/marketing/contacts",
  method: "PUT",
  body: { list_ids: [LIST_ID], contacts: [{ email, first_name: firstName }] },
});

// After (Loops)
await loops.updateContact({
  email,
  properties: { firstName },
  mailingLists: { [process.env.LOOPS_PRODUCT_UPDATES_LIST_ID!]: true },
});

Environment

# Before
SENDGRID_API_KEY=
SENDGRID_PRODUCT_UPDATES_LIST_ID=

# After
LOOPS_API_KEY=                          # Settings -> API
LOOPS_TRANSACTIONAL_ID_WELCOME=         # each published template's Publish page
LOOPS_TRANSACTIONAL_ID_PASSWORD_RESET=
LOOPS_TRANSACTIONAL_ID_PAYMENT_FAILED=
LOOPS_PRODUCT_UPDATES_LIST_ID=          # Audience -> Lists

Dashboard setup, the part code can’t do

  1. Generate an API key under Settings, then API. Sanity check it with GET https://app.loops.so/api/v1/api-key.

  2. Verify your sending domain: add the SPF, DKIM, and MX records, then verify. Sends only work from a verified domain and records can take up to 24 hours, so start here.

  3. Recreate each transactional email as a template, add its data variables with exact names (they are case sensitive), and publish. Drafts can’t send; the Publish page shows the ID and expected payload.

  4. Create your mailing lists and copy their IDs.

Cutover checklist

  • Export suppression and unsubscribe state from SendGrid and honor it from day one.

  • Run both providers in parallel and cut over per email type, not all at once.

  • Transactional sends don’t add recipients to your audience unless you pass addToAudience: true. Leave it off for password resets.

  • The rate limit is 10 requests per second per team, and the SDK throws on 429. An Idempotency-Key header with a 24 hour window makes retries safe.

  • Keep template variable names and code in sync. A send missing a required variable fails with a 400, or mark variables optional in the editor.

Common questions

Can I keep sending HTML defined in my code?

How long does the migration take?

Will my unsubscribes carry over?

Keep going

Greenfield instead of migrating? Start with transactional email from Next.js. Want your agent to run this whole guide? See send email from AI agents, or read how transactional email works on Loops.