Migrate/SendGrid
Structural API change

Switch from SendGrid to Anypost

SendGrid and Anypost share the same concepts: domains, API keys, sends, and delivery events. The APIs look different, though. The biggest change is SendGrid's personalizations wrapper, which Anypost replaces with a flat, simpler JSON body.

~30 mintypical migration time
1structural change (flatten the body)
8official SDK languages

What's the same, what changed

Both platforms send authenticated email over HTTP and SMTP, report delivery events via webhooks, and authenticate with API keys. The main difference is how you structure a send request.

ConceptSendGridAnypost
Base URLapi.sendgrid.com/v3api.anypost.com/v1
Send endpointPOST /v3/mail/sendPOST /v1/email
Auth headerAuthorization: Bearer SG.…Authorization: Bearer ap_…
Key prefixSG.ap_
Request bodypersonalizations array + top-level from / contentFlat: from, to, subject, html, text
to / cc / bccInside personalizations[0]Top-level to, cc, bcc
subjectpersonalizations[0].subject or top-levelTop-level subject
html / text bodycontent: [{type, value}] arrayhtml / text (flat fields)
reply_toreply_to: {email, name}reply_to: "addr" or array
AttachmentsBase64 encodedBase64 encoded (same shape)
Batch sendMultiple personalizations in one requestPOST /v1/email/batch
Tags / categoriescategories: ["string"]tags: ["string"]
Dynamic templatestemplate_id + dynamic_template_datatemplate_id + variables
Successful response202 Accepted, no body202 Accepted, {id, created_at}
WebhooksEvent Webhook, flat event namesemail.* namespaced names, batched
SMTP hostsmtp.sendgrid.netsmtp.anypost.com
SMTP usernameapikey (literal string)anypost
SMTP passwordYour SG.… keyYour ap_… key

The personalizations wrapper is the key change. SendGrid wraps recipients, subject, and per-message substitutions in a personalizations array. Anypost uses a flat body: from, to, subject, html/text at the top level. Most other differences follow from this.

Five steps to switch

You can run SendGrid and Anypost in parallel during cutover; they are independent services. Switch one email type at a time and verify delivery before cutting over completely.

Create an Anypost account and verify your domain

Sign up at anypost.com and add the same sending domain you use with SendGrid. You'll publish DNS records for SPF, DKIM, and tracking, then click verify. Propagation takes a few minutes to a few hours. Your SendGrid domain setup doesn't transfer, so configure the domain fresh in Anypost. See Domains.

Create an API key

In the Anypost dashboard, create a key (it starts with ap_). Use a send-only key for your application server. Set it as ANYPOST_API_KEY alongside your existing SENDGRID_API_KEY during cutover. See Authentication.

Swap the package and flatten your send call

Replace your SendGrid SDK with the Anypost SDK, then rewrite the send call. The main work is converting SendGrid's personalizations structure to Anypost's flat fields. The content array ([{type, value}]) becomes top-level html / text fields.

Before (SendGrid)
import sgMail from "@sendgrid/mail";
 
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
 
await sgMail.send({
  personalizations: [{
    to: [{ email: "[email protected]" }],
    subject: "Hello",
  }],
  from: { email: "[email protected]" },
  content: [{
    type: "text/html",
    value: "<p>Hi there.</p>",
  }],
});
After (Anypost)
import { Anypost } from "anypost";
 
const anypost = new Anypost(process.env.ANYPOST_API_KEY);
 
await anypost.email.send({
  from: "[email protected]",
  to: ["[email protected]"],
  subject: "Hello",
  html: "<p>Hi there.</p>",
});

Update webhook event names

Anypost reports the same delivery lifecycle as SendGrid, but the type strings differ; Anypost namespaces them under email.*. Update your switch statement to the new names. See the Webhooks section below for the full mapping.

Check your response handling

SendGrid returns 202 Accepted with an empty body. Anypost also returns 202, but with a JSON body: {"id": "email_…", "created_at": "…"}. The status code check still passes; capture the id to correlate delivery events later. See Send an email.

The same change in every SDK

The structural difference is consistent across every language: flatten personalizations and replace the content array with html / text fields. Pick your language.

SendGrid
curl https://api.sendgrid.com/v3/mail/send \
  -H "Authorization: Bearer SG.…" \
  -H "Content-Type: application/json" \
  -d '{
    "personalizations": [{
      "to": [{"email": "[email protected]"}],
      "subject": "Welcome"
    }],
    "from": {"email": "[email protected]"},
    "content": [{
      "type": "text/html",
      "value": "<p>Hi.</p>"
    }]
  }'
Anypost
curl https://api.anypost.com/v1/email \
  -H "Authorization: Bearer ap_…" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "[email protected]",
    "to": ["[email protected]"],
    "subject": "Welcome",
    "html": "<p>Hi.</p>"
  }'

Sending to multiple recipients: In SendGrid you'd add multiple personalizations entries (or several addresses inside one). In Anypost, pass multiple addresses in the to array, or use POST /v1/email/batch for fully independent per-recipient messages. See Batch sending.

Webhooks

Anypost uses email.* namespaced event types. The lifecycle is the same one you're already handling; the type strings change and the payload arrives in a batched events array.

Lifecycle momentSendGrid eventAnypost event type
Accepted / queuedprocessedemail.sentrename
Delivered to inboxdeliveredemail.deliveredrename
Temporary failure / retrydeferredemail.delayedrename
Hard bouncebounceemail.bouncedrename
Dropped before senddroppedemail.suppressedrename
Spam complaintspamreportemail.complainedrename
Email openedopenemail.openedrename
Link clickedclickemail.clickedrename
Unsubscribeunsubscribeemail.unsubscribedrename

Every SendGrid lifecycle event has an Anypost equivalent, including ones you may have treated as SendGrid-specific: processed becomes email.sent, deferred becomes email.delayed, and dropped (a suppression-list drop) becomes email.suppressed. SendGrid's subscription-group events (group_unsubscribe, group_resubscribe) have no analog; manage opt-outs through Anypost's suppression list instead.

Before (SendGrid)
// SendGrid posts an array of events
app.post("/webhook/email", (req, res) => {
  for (const event of req.body) {
    switch (event.event) {
      case "delivered":  markDelivered(event.sg_message_id); break;
      case "bounce":     suppressAddress(event.email); break;
      case "deferred":   scheduleRetryNotice(event.sg_message_id); break;
      case "spamreport": unsubscribe(event.email); break;
      case "open":       recordOpen(event); break;
      case "click":      recordClick(event.url); break;
    }
  }
  res.sendStatus(200);
});
After (Anypost)
// Anypost posts a batched events array; recipient is data.recipient
app.post("/webhook/email", (req, res) => {
  for (const { type, data } of req.body.events) {
    switch (type) {
      case "email.delivered":  markDelivered(data.email_id); break;
      case "email.bounced":    suppressAddress(data.recipient); break;
      case "email.delayed":    scheduleRetryNotice(data.email_id); break;
      case "email.complained": unsubscribe(data.recipient); break;
      case "email.opened":     recordOpen(data); break;
      case "email.clicked":    recordClick(data.tracking.url); break;
    }
  }
  res.sendStatus(200);
});

Signature verification differs. SendGrid signs its Event Webhook with an ECDSA public key; Anypost signs with an Anypost-Signature header (t=<unix>,v1=<hmac>) over the timestamp and raw body. Swap your verification step before trusting a delivery. See Webhooks.

Register your webhook URL once in the Anypost dashboard and select which events to receive. The HTTPS endpoint and 200-response contract work the same way; only the event strings and payload shape change.

Using SMTP instead of the API?

Update the host, username, and password. SendGrid's SMTP username is the literal string apikey; Anypost uses anypost, with your ap_ key as the password.

SendGrid SMTP
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASS=SG.…
SMTP_TLS=true
Anypost SMTP
SMTP_HOST=smtp.anypost.com
SMTP_PORT=587
SMTP_USER=anypost
SMTP_PASS=ap_…
SMTP_TLS=true

Anypost supports ports 587 (recommended), 2587, and 25. STARTTLS is required on every port, and there's no implicit-TLS port, so if your SendGrid config uses port 465, switch to 587. See Sending over SMTP.

Ready to switch?

Create your account, verify your domain, and send your first email in minutes.