Migrate from SendGrid
Step-by-step guide to moving from Twilio SendGrid to Sendbound. API mapping, template porting, webhook translation, and parallel-run strategy.
This guide covers everything specific to migrating from Twilio SendGrid — API differences, template syntax, event webhook mapping, and what to watch for.
Before starting, read the general migration guide for the parallel-run strategy and DNS playbook that applies to all providers.
What's different
| SendGrid | Sendbound | |
|---|---|---|
| API base | https://api.sendgrid.com/v3 | https://api.sendbound.com/v1 |
| Auth header | Authorization: Bearer SG.xxx | Authorization: Bearer sk_live_xxx |
| Send endpoint | POST /mail/send | POST /send |
| Template syntax | Handlebars {{variable}} | Handlebars {{variable}} ✓ Same |
| Dynamic templates | Separate template ID + dynamic_template_data | Inline or stored template with variables |
| Webhook events | Multiple event types per payload item | One event per webhook call |
| Unsubscribe groups | Built-in group management | Suppression lists + segments |
Export from SendGrid
Export templates
- Marketing → Email Designs → download each design as HTML
- Transactional → Dynamic Templates → open each template → download HTML version
Store the HTML files locally — you'll import them into the Sendbound template editor.
Export contacts
- Marketing → Contacts → All Contacts
- Click Export → wait for the CSV email
- Also export Unsubscribes and Bounces from Suppressions — import these first in Sendbound
Note your webhook configuration
- Settings → Mail Settings → Event Webhook
- Copy the URL you're currently posting to
- Note the event types you have enabled
- Copy the Signed Webhook Verification Key if you're verifying signatures
API migration
Sending a transactional email
import sgMail from '@sendgrid/mail';
sgMail.setApiKey(process.env.SENDGRID_API_KEY!);
await sgMail.send({
to: 'user@example.com',
from: 'hello@yourdomain.com',
subject: 'Welcome to the app',
html: '<p>Welcome aboard!</p>',
});// No SDK needed
await fetch('https://api.sendbound.com/v1/send', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SENDBOUND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: 'user@example.com',
from: 'hello@yourdomain.com',
subject: 'Welcome to the app',
html: '<p>Welcome aboard!</p>',
}),
});Using a stored template
await sgMail.send({
to: 'user@example.com',
from: 'hello@yourdomain.com',
templateId: 'd-abc123',
dynamicTemplateData: {
first_name: 'Jane',
confirm_url: 'https://app.example.com/confirm/xyz',
},
});await fetch('https://api.sendbound.com/v1/send', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SENDBOUND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: 'user@example.com',
from: 'hello@yourdomain.com',
templateId: 'tmpl_abc123', // Sendbound template ID from template editor
variables: {
first_name: 'Jane',
confirm_url: 'https://app.example.com/confirm/xyz',
},
}),
});Sending to multiple recipients
await sgMail.sendMultiple({
to: ['alice@example.com', 'bob@example.com'],
from: 'hello@yourdomain.com',
subject: 'Hello everyone',
html: '<p>Hi team!</p>',
});// Send individually for tracking per-recipient
await Promise.all(
['alice@example.com', 'bob@example.com'].map(to =>
fetch('https://api.sendbound.com/v1/send', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SENDBOUND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ to, from: 'hello@yourdomain.com', subject: 'Hello everyone', html: '<p>Hi team!</p>' }),
})
)
);Template migration
SendGrid dynamic templates use Handlebars ({{variable}}). Sendbound uses the same syntax — most templates work without modification.
What works unchanged
Hello {{first_name}},
{{#if trial_active}}
Your trial ends on {{trial_end_date}}.
{{else}}
Your subscription is active.
{{/if}}What needs updating
| SendGrid | Sendbound | Notes |
|---|---|---|
{{{unsubscribe}}} | {{unsubscribe_url}} | Rename the variable |
{{asm_group_unsubscribe_raw_url}} | {{unsubscribe_url}} | Sendbound uses one universal unsubscribe URL |
{{sendgrid_unsubscribe_preferences_raw_url}} | Remove or replace with your preference center URL |
Webhook migration
Update your endpoint
In Sendbound: Developer → Webhooks → Add endpoint. Enter the same URL your SendGrid webhook was posting to.
Event name mapping
| SendGrid event | Sendbound event |
|---|---|
processed | (not emitted — delivery is the confirmation) |
delivered | email.delivered |
open | email.opened |
click | email.clicked |
bounce | email.bounced |
dropped | email.bounced (with reason: dropped) |
spamreport | email.complained |
unsubscribe | email.unsubscribed |
group_unsubscribe | email.unsubscribed |
Signature verification
import { EventWebhook, EventWebhookHeader } from '@sendgrid/eventwebhook';
const ew = new EventWebhook();
const key = ew.convertPublicKeyToECDH(process.env.SENDGRID_WEBHOOK_KEY!);
const valid = ew.verifySignature(
key,
payload,
req.headers[EventWebhookHeader.SIGNATURE] as string,
req.headers[EventWebhookHeader.TIMESTAMP] as string,
);import crypto from 'crypto';
function verifyWebhook(payload: string, signature: string, secret: string) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
}
// In your handler:
const isValid = verifyWebhook(
rawBody,
req.headers['x-sendbound-signature'] as string,
process.env.SENDBOUND_WEBHOOK_SECRET!,
);SPF record update
# Before (SendGrid only)
v=spf1 include:sendgrid.net ~all
# During parallel run (keep both)
v=spf1 include:sendgrid.net include:sendbound.com ~all
# After cutover (remove SendGrid)
v=spf1 include:sendbound.com ~allChecklist
- Exported all templates from SendGrid
- Exported contact list + suppression lists
- Added and verified sending domain in Sendbound
- Imported suppression list (bounces + unsubscribes)
- Imported contacts
- Imported templates and verified rendering
- Updated API calls in staging
- Updated SPF record to include both providers
- Configured Sendbound webhook
- Updated webhook signature verification code
- Ran parallel test (both providers live)
- Cut over to Sendbound in production
- Removed SendGrid
include:from SPF - Revoked SendGrid API key
- Cancelled SendGrid plan
Migration Guide
Move from SendGrid, Mailgun, Postmark, or AWS SES to Sendbound. Parallel-run strategy, DNS cutover, and rollback plan included.
Migrate from Mailgun
Step-by-step guide to moving from Sinch Mailgun to Sendbound. API mapping, domain configuration, webhook translation, and parallel-run strategy.

