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 template |
| 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_ID
The 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.csv
Export 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',
});
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',
});
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');
}
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