Migrate from SendGrid to Unitpost

Medium migrationContacts, bounces, spam reports, global unsubscribes and dynamic templates import, but unsubscribe-group opt-outs and segments do not, so you copy those by hand before the first campaign.

SendGridUnitpost
ContactsImports
Suppressions (bounces and complaints)Imports
Unsubscribes and consentPartly imports
API keysRebuild

What comes with you

To leave SendGrid, paste a SendGrid API key into Unitpost's Migration Assistant. It exports your Marketing contacts with custom fields and list membership, imports bounces, spam reports, blocks and global unsubscribes, and brings dynamic templates, authenticated domains and the event webhook. Unsubscribe-group opt-outs and segments do not import, so copy group opt-outs before any campaign. Send history, Single Sends, automations, subusers and IP pools stay at SendGrid.

DataResultWhat happens
ContactsImportsThe importer runs SendGrid's contact export (once for the account, once per list) and imports every Marketing contact with name and custom fields under their field names. Addresses you only emailed through Mail Send are not Marketing contacts unless you added them.
Suppressions (bounces and complaints)ImportsBounces and spam reports import with their reason. Blocks import too, as suppressions that stop every send, although SendGrid itself stops suppressing a blocked address. Invalid emails are not read.
Unsubscribes and consentPartly importsGlobal unsubscribes import as unsubscribe suppressions, and those contacts land unsubscribed, which stops marketing here. Unsubscribe-group opt-outs are not read; step 2 copies them.
Lists and segmentsPartly importsMarketing lists become static segments with their members. SendGrid segments are not read, even though the preview shows a notice about them; rebuild them as dynamic segments.
TopicsPartly importsEach unsubscribe group becomes a topic that contacts are enrolled in by default, as groups work. Nobody's group opt-out comes with it.
TemplatesPartly importsDynamic templates import the active version's subject, HTML and text as drafts in code mode. Plain {{name}} placeholders work; Handlebars helpers such as {{#if}} and {{#each}} do not, and legacy templates are not read.
Sending domainsPartly importsAuthenticated domains are recreated as verifying with a new DKIM key. Link branding is not read.
WebhooksPartly importsOnly the webhook behind SendGrid's single-webhook settings endpoint is read, so accounts with several Event Webhooks lose the rest. It is recreated switched off with a new signing secret.
API keysRebuildKeys cannot be read. Create Unitpost keys with the scopes each service needs.
Single SendsRebuildNot read. Recreate scheduled ones as campaigns on a segment.
SubusersRebuildThe importer reads only the account its key belongs to. Unitpost has no subusers, so give each subuser its own workspace and run the importer with a key from that subuser.
Send history and analyticsStays behindEmail Activity, stats and Single Send results stay in SendGrid.
AutomationsStays behindMarketing automations stay in SendGrid; Unitpost automations are not launched yet.
IP pools and dedicated IPsStays behindIP pools, dedicated IPs and their warmup history stay at SendGrid; nothing imports them.

Before you start

  • A SendGrid API key with Full Access, or a Restricted Access key with the read scopes listed under the key field in Settings → Migration. A narrower key skips whatever it cannot read.
  • A Unitpost API key with emails:send for your app, created in API keys.
  • A second Unitpost key for the migration steps, with templates:write, topics:read, topics:write and webhooks:manage, saved as UNITPOST_MIGRATION_KEY. Keep it out of your app and revoke it once the move is done.
  • Access to your DNS host.
  • If you use subusers, a decision on which workspace each one becomes.

The cut-over plan

SendGrid keeps sending until the last step, so there is no gap in delivery.

  1. Run the Migration Assistant

    Paste the key in Settings → Migration. The snapshot waits on SendGrid's contact export jobs, so a large account takes longer. In the preview's suppression list, SendGrid blocks show as other: exclude the ones you still want to mail before you accept. Suppressions are written before contacts, so globally unsubscribed contacts land unsubscribed.

  2. Copy unsubscribe-group opt-outs

    Do this before any campaign scoped to a topic. Read every group unsubscribe from GET /v3/asm/suppressions and opt those addresses out of the matching topic with POST /api/v1/email/topics/{id}/opt-out. Each address must already be a contact here: the call refuses the whole list at the first one that is not.

    copy-group-unsubscribes.mjs
    import { Unitpost } from "unitpost";
    
    const unitpost = new Unitpost(process.env.UNITPOST_MIGRATION_KEY);
    
    // 1. Every group unsubscribe: one row per address per group.
    const rows = await fetch("https://api.sendgrid.com/v3/asm/suppressions", {
      headers: { Authorization: "Bearer " + process.env.SENDGRID_API_KEY },
    }).then((r) => r.json());
    
    // 2. The importer named each topic after its unsubscribe group.
    for await (const topic of unitpost.email.topics.listAll()) {
      const emails = rows
        .filter((r) => r.group_name === topic.name)
        .map((r) => r.email.toLowerCase());
      for (let i = 0; i < emails.length; i += 1000) {
        const { error } = await unitpost.email.topics.optOut(topic.id, {
          emails: emails.slice(i, i + 1000),
        });
        if (error) throw new Error(topic.name + ": " + error.message);
      }
    }
  3. Verify your domains next to SendGrid

    Open Domains and publish the records shown: MX and SPF on unitpost.<your domain> and a DKIM TXT under _domainkey. SendGrid's em and s1/s2._domainkey CNAMEs stay where they are, so both providers verify at once. Keep your single _dmarc record.

  4. Port dynamic templates

    Unitpost fills {{name}} placeholders only, with no loops or conditionals, so render {{#each}} tables in your code or split the template. first_name, last_name, email and unsubscribe_url are reserved and fill per recipient, so rename variables that use them and replace {{{unsubscribe}}} with {{unsubscribe_url}}. Imports land as marketing drafts, which adds a footer: set receipts and resets to transactional and publish with PATCH /api/v1/email/templates/{id}.

  5. Switch the SDK

    The display name moves into from (Acme <hi@acme.com>). A template send passes template: { id, variables } with Unitpost's tmpl_ id instead of template_id and dynamic_template_data, and scheduled_at (ISO 8601, up to 90 days ahead) replaces the Unix send_at, which SendGrid caps at 72 hours. Sends also accept an Idempotency-Key, which SendGrid does not offer.

    send.ts
    // Before: SendGrid
    const sgMail = require("@sendgrid/mail");
    sgMail.setApiKey(process.env.SENDGRID_API_KEY);
    await sgMail.send({
      to: "user@example.com",
      from: "hi@acme.com",
      subject: "Your receipt",
      html: receiptHtml,
    });
    
    // After: Unitpost
    import { Unitpost } from "unitpost";
    const unitpost = new Unitpost(process.env.UNITPOST_API_KEY);
    const { data, error } = await unitpost.email.send({
      to: "user@example.com",
      from: "Acme <hi@acme.com>",
      subject: "Your receipt",
      html: receiptHtml,
    });
  6. Replace the webhook verifier

    SendGrid's Signed Event Webhook is checked against a public key; Unitpost signs each delivery with a per-endpoint whsec_ secret in the svix format, one event per request with a type field. Rotate the secret in Webhooks to see it, deploy the new verifier, then set status to enabled. A switched-off endpoint does not queue events.

  7. Send tests

    Send each template once with POST /api/v1/email/templates/{id}/test-send, and fire POST /api/v1/webhooks/{id}/test at the endpoint.

  8. Move traffic in stages

    Move one message type first and watch email.bounced and email.complained for it, then the rest. SendGrid stays verified, so rolling back is a config change.

  9. Cancel SendGrid last

    Re-run step 2 on cut-over day, finish the checks below, then downgrade or close the account.

Name changes

Same ideas, new names

SendGridUnitpostNote
Marketing listSegment (static)
SegmentSegment (dynamic)Rebuilt by hand.
Unsubscribe groupTopicImported with default_opt_in: true.
Global unsubscribeUnsubscribed contactStops marketing; transactional still sends.
Dynamic templateTemplate
Single SendCampaign
Custom fieldContact field
Domain authenticationDomain
SubuserSeparate workspace
CategoryTag (tags: [{ name, value }])

API calls

SendGrid APIUnitpost APINote
POST /v3/mail/sendPOST /api/v1/email
personalizations (up to 1,000 per call)POST /api/v1/email/batchUp to 100 messages per call, each personalized.
template_id + dynamic_template_datatemplate: { id, variables }
send_at (Unix time, up to 72 hours ahead)scheduled_at (ISO 8601 with a zone, up to 90 days ahead)
PUT /v3/marketing/contactsPOST /api/v1/contacts/importsUp to 30,000 contacts there, 5,000 rows here.
GET /v3/suppression/bouncesGET /api/v1/suppressions?reason=bounce
GET /v3/suppression/spam_reportsGET /api/v1/suppressions?reason=complaint
POST /v3/asm/groups/{group_id}/suppressionsPOST /api/v1/email/topics/{id}/opt-out
GET /v3/templates/{template_id}GET /api/v1/email/templates/{id}
GET /v3/user/webhooks/event/settings/allGET /api/v1/webhooks

SMTP settings

SendGrid SMTPUnitpost SMTPNote
smtp.sendgrid.netsmtp.unitpost.com
Username apikeyUsername unitpost
Password: API key with Mail permissionPassword: Unitpost API key with emails:send
25, 587, 2525 (TLS optional)587, 2587, 25 (STARTTLS)TLS 1.2 or later is required before login.
465 (SSL)465, 2465 (implicit TLS)

Webhook events

SendGrid eventUnitpost eventNote
processedemail.sent
deliveredemail.delivered
deferredemail.delivery_delayed
bounceemail.bounced
droppedemail.failedA send our suppression list stops is email.suppressed.
spamreportemail.complained
openemail.opened
clickemail.clicked
unsubscribecontact.updated
group_unsubscribecontact.updatedThe importer maps it here; topic.unsubscribed is the closer event.
group_resubscribecontact.updatedtopic.subscribed is the closer event.

Before you cancel SendGrid

  • Export contacts and suppression lists as files before you downgrade or close: when SendGrid retired its free plans, accounts left on them with more than 100 contacts had their contacts deleted.
  • Download the Email Activity you need; SendGrid keeps up to 30 days of it only with its Email Activity history add-on, and none of it moves.
  • Re-run the group unsubscribe copy (step 2) so late opt-outs reach Unitpost.
  • Delete SendGrid's em CNAME and s1._domainkey and s2._domainkey CNAMEs once no mail goes through SendGrid. Keep _dmarc and Unitpost's records.
  • Delete SendGrid API keys and switch off its Event Webhooks.

Facts about SendGrid checked 2026-11-02. Providers change their APIs and plans; check their docs before you rely on a detail.

Questions about leaving SendGrid

Does SendGrid still have a free plan?

No. SendGrid began retiring its free Email API and Marketing Campaigns plans on May 28, 2025, with a 60-day transition before sending paused on free accounts. Unitpost's free plan is 5,000 emails a month, up to 200 a day, with no card.

Do my unsubscribe groups and their opt-outs carry over?

The groups do, as topics. The opt-outs do not, so run the script in step 2 before you send a campaign scoped to one of those topics.

Will my dynamic template Handlebars still render?

Plain `{{name}}` placeholders do. Helpers like `{{#if}}` and `{{#each}}` are left as text, because Unitpost templates have no loops or conditionals.

Can SendGrid keep sending while my domain verifies on Unitpost?

Yes. Unitpost's records sit on `unitpost.<your domain>` and a new DKIM selector, beside SendGrid's CNAMEs, so the domain is verified on both until you remove SendGrid's records.

What does SendGrid still do that Unitpost does not?

Subuser management for multi-tenant sending, dedicated IP pools, a long record at very high volume, and a much larger integration catalogue. If one of those is a requirement, stay on SendGrid for it.

Leaving SendGrid?

Preview the migration on the free tier

Verify a domain, paste a read-only key, and see what would come across before anything is written.