Migrate from Postmark
Step-by-step guide to moving from Postmark to Sendbound. API mapping, message stream mapping, template porting, and webhook translation.
This guide covers everything specific to migrating from ActiveCampaign Postmark.
Before starting, read the general migration guide for the parallel-run strategy and DNS playbook.
What's different
| Postmark | Sendbound | |
|---|---|---|
| API base | https://api.postmarkapp.com | https://api.sendbound.com/v1 |
| Auth header | X-Postmark-Server-Token: xxx | Authorization: Bearer sk_live_xxx |
| Send endpoint | POST /email | POST /send |
| Template syntax | Mustache {{variable}} | Handlebars {{variable}} ✓ Compatible |
| Message streams | Outbound / Broadcasts | Transactional send / Campaigns |
| Template call | POST /email/withTemplate | POST /send with templateId |
| Bounce types | HardBounce, SoftBounce, etc. | bounced with reason string |
Export from Postmark
Export templates via API
Postmark has no UI bulk export. Use their API:
# List all templates
curl -H "X-Postmark-Server-Token: YOUR_SERVER_TOKEN" \
https://api.postmarkapp.com/templates
# Fetch individual template
curl -H "X-Postmark-Server-Token: YOUR_SERVER_TOKEN" \
https://api.postmarkapp.com/templates/TEMPLATE_IDThe response includes HtmlBody and TextBody fields — save these as HTML files.
Export suppression list
curl -H "X-Postmark-Server-Token: YOUR_SERVER_TOKEN" \
"https://api.postmarkapp.com/suppressions/dump?MessageStream=outbound" \
> suppressions.csvExport contacts
Postmark doesn't have a contact database for Broadcasts users built in. Export your subscriber list from your own database or from the Broadcasts UI if available.
Note webhook (delivery webhook) configuration
Server Settings → Webhooks tab — note which events are enabled and the endpoint URL for each stream.
Message stream mapping
Postmark separates transactional (Outbound stream) and broadcast (Broadcasts stream) email. Sendbound handles both:
| Postmark stream | Sendbound equivalent |
|---|---|
| Outbound (transactional) | POST /v1/send |
| Broadcasts | Campaigns |
API migration
Sending a transactional email
import * as postmark from 'postmark';
const client = new postmark.ServerClient(process.env.POSTMARK_SERVER_TOKEN!);
await client.sendEmail({
From: 'hello@yourdomain.com',
To: 'user@example.com',
Subject: 'Welcome',
HtmlBody: '<p>Welcome aboard!</p>',
MessageStream: 'outbound',
});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',
html: '<p>Welcome aboard!</p>',
}),
});Using a stored template
await client.sendEmailWithTemplate({
From: 'hello@yourdomain.com',
To: 'user@example.com',
TemplateAlias: 'welcome',
TemplateModel: {
product_name: 'Acme',
user_name: 'Jane',
action_url: 'https://app.example.com/confirm/xyz',
},
MessageStream: 'outbound',
});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_your_id',
variables: {
product_name: 'Acme',
user_name: 'Jane',
action_url: 'https://app.example.com/confirm/xyz',
},
}),
});Template migration
Postmark uses Mustache ({{variable}}). Sendbound uses Handlebars which is a superset — all Mustache templates work unchanged.
| Postmark | Sendbound | Notes |
|---|---|---|
{{variable}} | {{variable}} | ✓ No change |
{{#if condition}} | {{#if condition}} | ✓ No change |
{{{html_variable}}} | {{{html_variable}}} | ✓ Triple braces for unescaped HTML work |
{{product.url}} (Postmark's action block) | Inline anchor tag | Replace action blocks with plain HTML buttons |
Postmark's "Action" layout blocks (buttons, call-to-action sections) are proprietary template components. Export the rendered HTML from Postmark's template preview and import that directly — you get the same visual output without the proprietary syntax.
Webhook migration
Event name mapping
| Postmark event | Sendbound event |
|---|---|
Delivery | email.delivered |
Open | email.opened |
Click | email.clicked |
Bounce (HardBounce) | email.bounced |
Bounce (SoftBounce) | email.bounced (with reason) |
SpamComplaint | email.complained |
SubscriptionChange | email.unsubscribed |
Signature verification
// Postmark uses a shared secret in the header
// but does not sign the payload body
const webhookSecret = process.env.POSTMARK_WEBHOOK_TOKEN;
if (req.headers['x-postmark-webhook-token'] !== webhookSecret) {
return res.status(401).send('Unauthorized');
}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));
}
const isValid = verifyWebhook(
rawBody,
req.headers['x-sendbound-signature'] as string,
process.env.SENDBOUND_WEBHOOK_SECRET!,
);
if (!isValid) return res.status(401).send('Unauthorized');Checklist
- Exported all templates via Postmark API
- Exported suppression list (both streams)
- Noted which templates are Outbound vs Broadcasts
- Added and verified sending domain in Sendbound
- Imported suppression lists
- Imported templates (replaced Postmark action blocks with HTML)
- Updated API calls (removed
MessageStream, swapped header) - Configured Sendbound webhook for delivery events
- Updated webhook signature verification
- Ran parallel test
- Cut over
- Revoked Postmark server token
Migrate from Mailgun
Step-by-step guide to moving from Sinch Mailgun to Sendbound. API mapping, domain configuration, webhook translation, and parallel-run strategy.
Migrate from AWS SES
Step-by-step guide to moving from Amazon Simple Email Service (SES) to Sendbound. SDK replacement, IAM cleanup, SNS notification migration, and SMTP relay removal.

