Migrate from SendGrid
Migrating from SendGrid to Sendpository
Everything that changes when you move your email from SendGrid, in the order you'll do it. An afternoon for most codebases - the request shape is the main rewrite.
In short
Migrating from SendGrid to Sendpository means flattening the request: SendGrid's personalizations, from object and content array become flat from, to, subject, html and text fields. Sendpository returns 200 with the message id in the body instead of an empty 202, signs webhooks with HMAC-SHA256 instead of ECDSA, and sends one event per request instead of an array.Or let your coding agent do the whole migration
Paste it in. It finds every SendGrid call, rewrites it, updates your webhook handler and switches over behind a flag you can flip back.
Show the promptHide the prompt
Switch this project's email from SendGrid to Sendpository. First run `npx sendpository@latest init` - it opens my browser for me to approve, saves the API key and installs the Sendpository skill - then follow the skill's migration guide (references/migrate.md). If you can't run commands, read https://www.sendpository.com/agents/sendpository/references/migrate.md instead.
1. API differences
| SendGrid | Sendpository | |
|---|---|---|
| Endpoint | POST https://api.sendgrid.com/v3/mail/send | POST https://api.sendpository.com/v1/emails |
| Recipients | personalizations: [{ to: [{ email, name }] }] | to: ["Name <addr>"] - plain strings, cc and bcc alongside |
| Sender | from: { email, name } | from: "Name <addr>" |
| Body | content: [{ type: "text/html", value }] | html and text fields |
| Metadata | categories and custom_args | tags: a string key/value object |
| Scheduling | send_at (unix timestamp) | scheduled_at (ISO 8601), cancellable |
| Response | 202 Accepted, empty body; the message id is in the X-Message-Id header | 200 with { "id": "..." } in the body |
2. Authentication differences
| SendGrid | Sendpository | |
|---|---|---|
| API key | Authorization: Bearer SG.... | Authorization: Bearer sp_... - a sending key for the app, full_access only for managing domains and webhooks by API. |
Create the key under API keys in the dashboard. It is shown once, so put it straight into SENDPOSITORY_API_KEY in your secrets manager. Keep the SendGrid key until the switch is finished, then revoke it.
3. Domain setup
Add your sending domain in Sendpository and publish the records it shows: a 2048-bit DKIM key signed as your own domain, plus SPF and a bounce MX on a sp. subdomain so your existing records are never edited. DMARC is recommended; if you already publish one, leave it as it is.
- SendGrid's domain authentication uses CNAMEs on its own subdomains. Sendpository's records are separate, so you can add them without removing SendGrid's.
- If you used a SendGrid link-branding subdomain, keep it until old emails' links have stopped being clicked.
Not sure what’s published already? Run the domain through the email DNS checker.
4. Webhooks
| SendGrid | Sendpository | |
|---|---|---|
| Verification | Signed Event Webhook: ECDSA with a public key, in X-Twilio-Email-Event-Webhook-Signature and -Timestamp | HMAC-SHA256 with your endpoint secret, in Sendpository-Signature: t=<unix>,v1=<hex> |
Event names
| SendGrid event | Sendpository event |
|---|---|
| processed | email.sent |
| delivered | email.delivered |
| deferred | email.delayed |
| bounce | email.bounced |
| dropped | email.failed |
| spamreport | email.complained |
| open | email.opened |
| click | email.clicked |
| unsubscribe | email.unsubscribed |
SendGrid posts an array of events; Sendpository posts one event per request, so drop the loop.
Full payloads and verification code are in the webhooks guide.
5. Error handling
| SendGrid | Sendpository | |
|---|---|---|
| Error shape | { "errors": [{ "message", "field", "help" }] } | { "error": { "type": "domain_not_verified", "message": "..." } } |
Sendpository returns a single error with a stable type. Branch on error.type rather than parsing messages. Only rate_limit_exceeded (wait for Retry-After) and internal_error are worth retrying automatically; the errors reference lists every type.
6. Code migration
await fetch("https://api.sendgrid.com/v3/mail/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
personalizations: [{ to: [{ email: "customer@example.com" }] }],
from: { email: "billing@yourdomain.com", name: "Acme" },
subject: "Your receipt",
content: [{ type: "text/html", value: "<p>Thanks for your order.</p>" }],
categories: ["receipt"],
}),
}); // 202, empty bodyconst res = await fetch("https://api.sendpository.com/v1/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SENDPOSITORY_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "receipt-10428",
},
body: JSON.stringify({
from: "Acme <billing@mail.yourdomain.com>",
to: ["customer@example.com"],
subject: "Your receipt",
html: "<p>Thanks for your order.</p>",
text: "Thanks for your order.",
tags: { type: "receipt" },
}),
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.type}: ${body.error.message}`);
console.log(body.id);Watch out for
- A 202 with no body is gone - read the id from the JSON response and store it if you look messages up later.
- Personalizations that sent different content to several recipients in one call become separate messages, or one POST to /emails/batch with up to 100.
- Dynamic templates (template_id with dynamic_template_data) have no equivalent - render the HTML in your app before sending.
- Your event handler must accept a single JSON object, not an array.
7. Pricing
| SendGrid | Sendpository | |
|---|---|---|
| Published pricing | The permanent free plan was retired in 2025 in favour of a 60-day trial (100 emails a day). Essentials starts at $19.95 a month for up to 100,000 emails. | 6,000 emails a month (up to 200 a day), free forever. Current plans |
SendGrid pricing from their own pages, checked 2026-09-23. Sendpository has a permanent free plan (6,000 emails a month); SendGrid does not.
8. Things to test before switching
Specific to leaving SendGrid:
- Flatten any `personalizations` array. SendGrid's send body nests recipients and substitutions inside it; ours takes `to`, `cc` and `bcc` at the top level, so a multi-recipient send ported verbatim will send to nobody.
- Re-point domain authentication. SendGrid authenticates with CNAMEs aimed at their infrastructure, so your DKIM key lives with them; ours is a TXT record on your own apex. Leave theirs in place until our domain verifies.
- Rewrite Dynamic Template calls. There is no `template_id` on our send - render your Handlebars templates in your own code and post the resulting HTML.
True of any migration:
- Send to a real Gmail and a real Outlook address and open Show original: SPF, DKIM and DMARC should all say PASS, with DKIM signed as your own domain.
- Send a message with an attachment and one with an inline image, and check both render.
- Trigger a bounce (an address at a domain you control with no mailbox) and confirm your webhook handler receives email.bounced and marks the address.
- Send the same request twice with one Idempotency-Key and confirm only one email arrives.
- Point a staging environment at the new API key first, and watch the Logs page for 4xx responses before switching production.
- Keep the old provider's DNS records until the new domain has verified and a day of production mail has been delivered - removing them first breaks mail still in flight.
Questions
Does Sendpository support SendGrid dynamic templates?
No. Render your template in your own code - Handlebars, MJML, React Email or plain strings - and send the HTML. It keeps templates in version control next to the code that fills them.
Can I migrate SendGrid's suppression list?
Yes. Export bounces, blocks and spam reports from SendGrid and add them on Sendpository's Suppressions page, so you never mail those addresses again.
Is there an SMTP relay like SendGrid's?
Sendpository is an HTTPS API. Most frameworks have an HTTP mail transport or a small adapter; the guides have examples for Node.js, Python, PHP, Laravel, Go, Rails and Java.
SendGrid API details are from their documentation, checked 2026-09-25: www.twilio.com, www.twilio.com, www.twilio.com.
Keep reading
Sendpository vs SendGrid
Where each is the better choice, side by side.
Migrate from Resend
Usually under an hour for the code, plus DNS propagation.
Migrate from Postmark
An hour or two - the request maps field for field.
Migrate from Mailgun
An afternoon - mostly swapping form-data for JSON.
Send your first email in minutes
6,000 emails a month (up to 200 a day), free forever. No card needed.