Skip to main content
A cutover, not a sync. Contacts, suppressions, templates, and API calls move once, then nothing stays connected to Postmark. The rule behind every step below: only one platform sends live email to a given contact at a time. This guide assumes your Brew brand and sending domain are already set up. If not, verify your domain first. DNS can take up to 72 hours.
Looking to go the other way, designing in Brew and sending from Postmark? See the Postmark integration instead.
Not ready to commit? Leave your Postmark calls in place and route one new one-off email through Brew instead. Steps 1 to 5 cover contacts and a template, which is all you need for a single test send.

Concepts

Check These Limits First

Sending mostly transactional mail through Postmark? Three Brew limits matter most for that kind of sending:
  • 60 fires a minute. Trigger fires share the automation.runs.write rate limit: 60 requests per credential over a rolling 60 seconds. A busy receipt or reset stream can pass that. See Rate Limits.
  • No attachments. Brew sends and trigger fires have no attachment field. A PDF invoice or a calendar invite can’t ride along.
  • No delivery webhooks yet. Webhooks for delivery, open, and bounce events are on the roadmap. Today you poll GET /v1/sends and GET /v1/analytics/events instead.
If a Postmark stream depends on any of these, keep that stream on Postmark for now. Move broadcast mail first.
Every marketing recipient must have opted in to hear from you. Importing or syncing contact data into Brew is not permission to email it. See What You Can Send.
Postmark has no contact database. It sends to whatever address your code passes in each call, so there’s no contact list to export. Your contacts come from your own app: the users table, your CRM, or wherever you keep signups. Export them from there. Keep these columns:
  • Email, first name, last name, and any field your templates use
  • Your marketing opt-in flag, with the date the person opted in, if your app stores one
Only people who opted in to marketing belong in a marketing audience. An account in your app, or a receipt sent through Postmark, isn’t consent. Import the second group anyway. A trigger fire for an address Brew doesn’t know creates a subscribed contact. Anyone you leave out would land in your marketing audience the first time Brew sends them a receipt.

Export Suppressions per Stream

Postmark keeps a separate suppression list for each message stream. An address suppressed on your transactional stream can still be active on your broadcast stream, and the other way around. Export every stream’s list. In Postmark, open Servers, pick the server, open a message stream, then its Suppressions tab, then Export. The file is JSON, not CSV, so convert it before Step 2. From code, GET /message-streams/{stream_id}/suppressions/dump returns the same list with each address’s SuppressionReason. On a broadcast stream, an unsubscribe is stored as ManualSuppression. The Suppressions tab labels it Unsubscribed. Subscribed: false is the brand-wide opt-out. A broadcast unsubscribe is narrower: the person left that one stream. In Brew, each marketing domain keeps its own unsubscribe list, which is the closer match. See Two Ways to Opt Out. Ran several broadcast streams and can’t tell which one a person meant to leave? Mark them Subscribed: false. Stopping too much mail is the safe mistake.
Subscribed: false stops marketing only. Transactional mail still reaches unsubscribed contacts, and an import can’t mark an address as bounced.Brew suppresses an address after its own first hard bounce. Until then, keep your app from firing receipts at addresses Postmark suppressed for a hard bounce.
Postmark’s suppressions say nothing about consent, and its activity history stays behind. Brew’s engagement-based audiences start counting from your import date. Bring Your Consent Records covers carrying the opt-in date over.

2. Map Your CSV

Notes:
  • Postmark’s suppressions name the address field EmailAddress. In your CSV, call that column Email and add a Subscribed column set to false.
  • Don’t import your opt-in flag as it is. Brew reads opt-outs only from a Subscribed column, so any other column becomes a plain custom property and those people import as subscribed.
  • New columns become camelCase properties. A Plan Type column becomes planType. Use that name in merge tags and filters.
  • Empty cells don’t overwrite existing data on update imports.
  • Duplicate rows: the last one wins, so dedupe first.
  • These names belong to built-in contact fields and can’t be used for custom properties: email, firstName, lastName, subscribed, createdAt, updatedAt, validationStatus, lastValidatedAt, importId, csvFileName.
  • Save as UTF-8. Garbled characters at the mapping step usually mean it wasn’t.
  • Dates: day-first dates such as 03/04/2026 (3 April) import correctly when you pick day first at the mapping step, and YYYY-MM-DD needs no choice. See date formats.
Full import behavior lives in Add Contacts.

3. Import into Brew

Audience → Add contacts → CSV Upload. Map columns, review, then import. Tick the validation checkbox first if the list hasn’t been checked recently. It costs 2 credits per address, which is worth it before warming up a new domain. Upload the opted-in contacts, the people without an opt-in, and the suppressions as separate files. That keeps Subscribed set correctly per batch. Broadcast unsubscribes go to a domain instead. Open Domains in the sidebar and pick the marketing domain that replaces your broadcast stream. Then click View list → Import CSV. Each file holds up to 10,000 rows. See Import a List from Another Provider. A contact’s Subscribed status says Brew may email them. A consent record says how and when they agreed. Carry both over.
  • Export the evidence. Postmark stores no consent, so the evidence lives in your app: the signup checkbox, its label, and the opt-in date. Keep those columns as custom properties.
  • In the app, tick the consent checkbox at the review step. Brew records consent once for the whole file, dated the day you import.
  • To keep each person’s original opt-in, import over the API instead. POST /v1/contacts takes a consent record per contact, with the original date as capturedAt. It never changes Subscribed.

4. Rebuild Segmentation

Postmark has no segments, so there’s little to rebuild. Your code picked who got each broadcast, usually with a database query. Rebuild each query as an Audience, a dynamic filter over contact properties, email engagement, and subscription status. Describe one in plain English or build the filters by hand. Import every property the query used, such as plan or lastActiveAt, in Step 3. Before cutover, check that each Audience’s count is close to what the query returns. A wrong translation can undercount or overcount who gets an email.

5. Rebuild Templates and Merge Tags

Postmark templates live on each server’s Templates tab. To pull them all at once, Postmark’s CLI runs postmark templates pull <path>. It writes each template’s HTML to content.html, with layouts in _layouts. From code, GET /templates/{templateIdOrAlias} returns one template’s HtmlBody, TextBody, and Subject. A template that uses a layout holds only the middle of the email. Copy the layout too, and paste the template’s body where the layout says {{{ @content }}}. Brew has no layouts. Each email carries its own header and footer, styled from your brand identity. In Brew, open Emails in the sidebar. The import tools sit in the toolbar on the right side of the canvas. See Importing existing designs. For most templates, use the HTML or screenshot as reference. Prompt Brew to rebuild it against your brand rather than treating the import as finished. It’s faster, and the result inherits your brand identity automatically. Keep each GIF under 2 MB so it loads fast on a phone.

Translate Merge Tags

Postmark templates use Mustachio. Brew runs Liquid. Most values in a template came from the TemplateModel in your API call. They become fields of the trigger’s payload, which Liquid reads under trigger..
  • {{name}} becomes {{ trigger.name }}. Declare each field on the trigger, because an undeclared key never reaches the email. Names are case-sensitive.
  • {{company.name}} becomes {{ trigger.company.name }}. Declare the nesting on the trigger. See Nested Payloads.
  • {{{name}}} becomes {{ trigger.name }}. Triple braces aren’t merge tags in Brew, and publish refuses most of them.
  • {{#each items}} … {{/each}} becomes {% for item in trigger.items %} … {% endfor %}. Inside the loop, Mustachio’s bare {{price}} becomes {{ item.price }}, and {{.}} becomes {{ item }}.
  • A parent-scope tag such as {{../company_name}} becomes the full path, {{ trigger.company_name }}.
  • {{#show_receipt}} … {{/show_receipt}} becomes {% if trigger.show_receipt %} … {% endif %}. An inverted {{^show_receipt}} becomes {% unless trigger.show_receipt %}.
  • A value about the person, not the event, can come from the contact instead. Write {{ firstName | there }}, and the fallback after the pipe shows when the contact has no name on file.
  • Delete {{{ @content }}} once the layout is folded in, and delete {{{ pm:unsubscribe }}}. Brew adds the unsubscribe link and your mailing address to every marketing email.

6. Rebuild Automations

Postmark has no automations. Every email it sent started with a call from your code. So this step is mostly setup for Step 7. Test-fire before publishing. Leave every rebuilt automation unpublished until cutover day, which is what prevents a double-send. Build an Automation covers the trigger setup and the test step.

7. Swap the API Calls in Your Code

Postmark’s API lives at https://api.postmarkapp.com. It takes a server token in an X-Postmark-Server-Token header. Brew uses one host, a bearer key, and plain JSON. Template sends. Each withTemplate call becomes a fire of the trigger you built in Step 6. Every published automation listening for it starts. Before, with Postmark:
After, with Brew:
What moves where:
  • To becomes the payload’s email. That’s the contact the automation runs for.
  • TemplateModel becomes the rest of the payload, read in the email as {{ trigger.order.total }}.
  • TemplateAlias, From, and MessageStream leave the call. The automation’s Send Email step holds the email, the subject, and the sending domain. The domain’s purpose does the job the stream did.
  • Open and click tracking follow the sending domain’s settings, not a flag on each call.
Postmark has no idempotency key, so a retried call can send twice. Brew’s Idempotency-Key header fixes that. Derive it from something stable about the action, like the order id above, so a repeat fire starts nothing new. See Idempotency. Postmark answers 200 once it accepts a message. Brew answers 202, which means queued, not delivered. The response lists automationRunIds. Follow one with GET /v1/automations/runs/{automationRunId}. Batch sends. A newsletter sent with /email/batchWithTemplates or the Bulk Email API (/email/bulk) becomes one Email sent to an Audience. Send it from the app, or from code with POST /v1/sends and an audienceId. Marketing opt-ins. Postmark never stored contacts, so this call is new. When someone opts in to marketing in your app, add them to Brew:
subscribed: true applies only to a new contact. A single upsert that asks for it on an opted-out contact gets 422 RESUBSCRIBE_NOT_ALLOWED, and nothing is written. Handle that 422 by sending the same call without subscribed. Full request shapes are in the API reference. This section shows the shape of the migration, not a copy-paste replacement for your code.

8. Cut Over

Move in this order:
  1. Rebuild while Postmark keeps sending. Import contacts and rebuild audiences, templates, and automations. None of it touches live sending. Keep every Brew automation unpublished.
  2. Check your segments. Confirm each Audience’s count against the query it replaces. See Step 4.
  3. Refresh opt-outs. Right before your first Brew send, export each stream’s new suppressions from Postmark and import them as in Step 3. Repeat before each type you move, until Postmark stops sending. Anyone who opted out during the rebuild stays opted out.
  4. Warm up with the first type you move. Stop sending that type through Postmark first, so nobody gets it from both. Send to your most engaged contacts first and ramp over several days. Gradual send does the ramping for you. No history means no reputation, even on a verified domain.
  5. Move one type at a time. Broadcasts first, then event-driven marketing email. For each template, in order: publish the Brew automation, then switch that call in your code from Postmark to the Brew fire. Watch each in analytics before you move the next.
  6. Move transactional email last. Skip this step for any stream a limit above rules out. Before you move any, add a separate transactional-purpose subdomain (see Transactional Email). A fire for a new address creates a subscribed contact, so only send marketing to people who opted in. Receipts and resets are the highest-stakes sends. Cut them over one at a time, and test-run each automation with a real payload before you point production code at its trigger.
  7. Keep Postmark as a fallback. Leave the account and its server token in place for a couple of weeks, so you can switch a call back. Then downgrade or cancel. If you receive mail through an inbound stream, keep it on Postmark or move it to an inbound provider.
Never leave the same email type live in both places at once. That’s how a contact gets the same receipt twice.

Quick Reference

Need Help?

Our team is ready to support you at every step of your journey with Brew. Choose the option that works best for you:

Search Documentation

Type in the “Ask any question” search bar at the top left to instantly find relevant documentation pages.

ChatGPT/Claude Integration

Click “Open in ChatGPT” at the top right of any page to explore it further with ChatGPT or Claude.