Sendry Docs
Guides

Migrating from SendGrid

Move your contacts, suppressions and sending code from SendGrid to Sendry.

SendGrid retired its permanent free plan for new signups in 2026, so a lot of teams are re-pricing their transactional email. This guide covers both halves of a move: the data (contacts and suppressions, handled by Sendry's importer) and the code (SDK and endpoint mapping).

Start with the data, because the importer needs nothing from you but a read-only API key.

1. Create a read-only SendGrid API key

In SendGrid: Settings → API Keys → Create API Key, choose Restricted Access, and grant read-only permission to:

PermissionWhy
MarketingLists, contacts, custom field definitions, and the contacts export job
SuppressionsBounces, blocks, spam reports and global unsubscribes

Nothing else is needed. Sendry never sends mail through your SendGrid account and never writes to it.

The key is validated the moment you paste it, with a single GET /v3/scopes call, so a typo or a revoked key fails immediately with a clear message instead of half an hour into an import. It is then sealed with AES-256-GCM (KMS-wrapped envelope encryption in production) before being stored, and is never written to logs.

2. Run the scan

In the Sendry dashboard, go to Migrate, pick SendGrid, and paste the key.

The scan is read-only and takes well under a minute on most accounts. It reports:

  • Marketing lists, each with its contact count. Every list you select becomes a Sendry audience of the same name.
  • All SendGrid contacts, a synthetic entry that imports every contact in the account into one audience. Useful if you keep no marketing lists, which is common for transactional-first accounts.
  • Suppression counts for bounces, blocks, spam reports and unsubscribes.
  • Custom field definitions, so you can see exactly what will land on each contact's metadata.

If your key is missing a permission, the scan says so on the review screen rather than silently returning an empty section.

3. Approve the import

Tick what you want and press Import selected. Two things are worth a moment's thought.

Blocks are unticked by default. A SendGrid block usually means the receiving server refused the message for reputation reasons, and that is frequently temporary. Importing blocks suppresses those addresses permanently in Sendry. Tick them only if you would rather be conservative than reach a possibly-recoverable address.

Contacts arrive through an export job. SendGrid's contacts export is asynchronous: Sendry starts the job, polls it with exponential backoff, downloads the file it produces and parses it. The contact counter can therefore sit still for a few minutes before it moves. That is the export building, not a stall.

The import is idempotent. A contact that already exists in Sendry is linked into the audience rather than duplicated; a suppression already on your list is left alone. If it fails part-way, Try again resumes: lists whose export already completed are skipped.

4. What comes across

Contacts

SendGrid export columnSendry contact field
EMAILemail, lowercased and trimmed
FIRST_NAMEfirst_name
LAST_NAMElast_name
CONTACT_IDmetadata.sendgrid_contact_id
PHONE_NUMBER, WHATSAPP, LINE, FACEBOOK, ALTERNATE_EMAILS, ADDRESS_LINE_1, ADDRESS_LINE_2, CITY, STATE_PROVINCE_REGION, POSTAL_CODE, COUNTRY, EXTERNAL_ID, UNIQUE_NAMEmetadata.<same name>
Marketing custom fieldsmetadata.<custom field name>, typed by the field's Text / Number / Date declaration
CREATED_AT, UPDATED_AT, ANONYMOUS_IDDropped. In Sendry created_at means "created here", so carrying the old value across would misreport your list's age

Sendry contacts have email, first_name and last_name columns plus a string metadata map, so everything SendGrid keeps that Sendry has no column for is preserved as metadata rather than discarded.

Suppressions

SendGrid listSendry reason
Bounceshard_bounce
Blockshard_bounce
Spam reportscomplaint
Global unsubscribesunsubscribe

Sendry keeps one suppression row per address, so an address on several SendGrid lists gets the strongest reason: complaint beats hard_bounce beats unsubscribe. Anything on the global unsubscribe list is also flagged unsubscribed on the contact record, so the opt-out is visible in both places.

What does not come across

  • Sending domains. SendGrid's authenticated domains publish SendGrid's own DNS records, so they cannot be moved. Add your domains under Domains in Sendry and publish the records it gives you. This is the one step you must do by hand.
  • API keys. Never migrated, for the obvious reason. Create fresh Sendry keys.
  • Webhooks. Re-register your endpoint URLs in Sendry after the move.
  • Templates. SendGrid dynamic templates are Handlebars ({{#if}}, {{#each}}, dotted paths, {{{raw}}}) and legacy templates use <%body%>. Sendry's HTML engine substitutes plain {{variable}} and nothing else, so an automatic translation would quietly break every template carrying logic. Recreate them under Templates, or send fully-rendered HTML from your own React Email components.

5. Plan limits

Contacts count against your plan's contact limit exactly as they do when you create them by hand. If an import hits the limit it stops the contact phase, keeps everything already imported, and marks the result partial with the limit named. Upgrade and press Try again to bring the rest across; nothing is duplicated.

Code migration

Package swap

npm remove @sendgrid/mail
npm install sendry-sdk

Initialization

// Before (SendGrid)
import sgMail from "@sendgrid/mail";
sgMail.setApiKey(process.env.SENDGRID_API_KEY);

// After (Sendry)
import { Sendry } from "sendry-sdk";
const sendry = new Sendry(process.env.SENDRY_API_KEY);

Sending

// Before (SendGrid)
await sgMail.send({
  to: "user@example.com",
  from: "hello@acme.com",
  subject: "Hello",
  html: "<p>Hello!</p>",
});

// After (Sendry)
const email = await sendry.emails.send({
  from: "hello@acme.com",
  to: "user@example.com",
  subject: "Hello",
  html: "<p>Hello!</p>",
});

Differences worth knowing:

  • Sendry takes from and to as plain strings or arrays of strings, not { email, name } objects. Use "Acme <hello@acme.com>" when you want a display name.
  • Sendry throws on failure instead of returning a response you have to inspect. Catch ApiError, RateLimitError and AuthenticationError from sendry-sdk.
  • Sendry returns the email object directly, with its id, so you can correlate webhooks without reading a header.

Error handling

// Before (SendGrid)
try {
  await sgMail.send(msg);
} catch (error) {
  console.error(error.response?.body?.errors);
}

// After (Sendry)
import { ApiError, RateLimitError, AuthenticationError } from "sendry-sdk";
try {
  const email = await sendry.emails.send(params);
} catch (error) {
  if (error instanceof AuthenticationError) {
    console.error("Bad API key");
  } else if (error instanceof RateLimitError) {
    await sleep(error.retryAfter * 1000);
  } else if (error instanceof ApiError) {
    console.error(error.statusCode, error.code, error.message);
  }
}

REST API endpoint mapping

FeatureSendGridSendry
Send emailPOST /v3/mail/sendPOST /v1/emails
Send batchPOST /v3/mail/send with multiple personalizationsPOST /v1/emails/batch
Get emailGET /v3/messages/:idGET /v1/emails/:id
List emailsGET /v3/messagesGET /v1/emails
Authenticate domainPOST /v3/whitelabel/domainsPOST /v1/domains
Validate domainPOST /v3/whitelabel/domains/:id/validatePOST /v1/domains/:id/verify
List domainsGET /v3/whitelabel/domainsGET /v1/domains
Create API keyPOST /v3/api_keysPOST /v1/api-keys
List API keysGET /v3/api_keysGET /v1/api-keys
ContactsPUT /v3/marketing/contactsPOST /v1/contacts
Lists / audiencesGET /v3/marketing/listsGET /v1/audiences
BouncesGET /v3/suppression/bouncesGET /v1/suppression
Spam reportsGET /v3/suppression/spam_reportsGET /v1/suppression
UnsubscribesGET /v3/suppression/unsubscribesGET /v1/suppression
Event webhookPOST /v3/user/webhooks/event/settingsPOST /v1/webhooks

Sendry folds SendGrid's four suppression endpoints into one GET /v1/suppression list with a reason field, which is the same shape the importer writes.

Environment variable rename

# .env
# Before
SENDGRID_API_KEY=SG.abc123

# After
SENDRY_API_KEY=sn_live_abc123

Webhook event names

EventSendGridSendry
Delivereddeliveredemail.delivered
Bouncedbounceemail.bounced
Blocked / droppedblocked, droppedemail.bounced
Openedopenemail.opened
Clickedclickemail.clicked
Spam reportspamreportemail.complained
Unsubscribeunsubscribe, group_unsubscribeNo direct event; the contact is flagged unsubscribed and added to your suppression list, and contact.updated fires

SendGrid posts an array of events per request; Sendry posts one signed event per request. Verify the signature as described in Webhook verification.

Checklist

  • Create a restricted SendGrid key with read access to Marketing and Suppressions
  • Run the scan under Migrate → SendGrid and review what was found
  • Import contacts and suppressions
  • Add your sending domains under Domains and publish Sendry's DNS records
  • Recreate templates that used Handlebars logic
  • Replace @sendgrid/mail with sendry-sdk
  • Change SENDGRID_API_KEY to SENDRY_API_KEY
  • Re-register webhook URLs and map the event names above
  • Send a test email, confirm delivery, then cut traffic over

On this page