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.
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.
| Concept | SendGrid | Anypost |
|---|---|---|
| Base URL | api.sendgrid.com/v3 | api.anypost.com/v1 |
| Send endpoint | POST /v3/mail/send | POST /v1/email |
| Auth header | Authorization: Bearer SG.… | Authorization: Bearer ap_… |
| Key prefix | SG. | ap_ |
| Request body | personalizations array + top-level from / content | Flat: from, to, subject, html, text |
| to / cc / bcc | Inside personalizations[0] | Top-level to, cc, bcc |
| subject | personalizations[0].subject or top-level | Top-level subject |
| html / text body | content: [{type, value}] array | html / text (flat fields) |
| reply_to | reply_to: {email, name} | reply_to: "addr" or array |
| Attachments | Base64 encoded | Base64 encoded (same shape) |
| Batch send | Multiple personalizations in one request | POST /v1/email/batch |
| Tags / categories | categories: ["string"] | tags: ["string"] |
| Dynamic templates | template_id + dynamic_template_data | template_id + variables |
| Successful response | 202 Accepted, no body | 202 Accepted, {id, created_at} |
| Webhooks | Event Webhook, flat event names | email.* namespaced names, batched |
| SMTP host | smtp.sendgrid.net | smtp.anypost.com |
| SMTP username | apikey (literal string) | anypost |
| SMTP password | Your SG.… key | Your 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.
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>",
}],
});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.
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>"
}]
}'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 moment | SendGrid event | Anypost event type | |
|---|---|---|---|
| Accepted / queued | processed | email.sent | rename |
| Delivered to inbox | delivered | email.delivered | rename |
| Temporary failure / retry | deferred | email.delayed | rename |
| Hard bounce | bounce | email.bounced | rename |
| Dropped before send | dropped | email.suppressed | rename |
| Spam complaint | spamreport | email.complained | rename |
| Email opened | open | email.opened | rename |
| Link clicked | click | email.clicked | rename |
| Unsubscribe | unsubscribe | email.unsubscribed | rename |
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.
// 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);
});// 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.
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASS=SG.…
SMTP_TLS=trueSMTP_HOST=smtp.anypost.com
SMTP_PORT=587
SMTP_USER=anypost
SMTP_PASS=ap_…
SMTP_TLS=trueAnypost 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.