Looking to go the other way, designing in Brew and sending from Postmark? See the Postmark integration instead.
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.writerate 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/sendsandGET /v1/analytics/eventsinstead.
1. Export Contacts, Consent, and Suppression
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
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.
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 columnEmailand add aSubscribedcolumn set tofalse. - Don’t import your opt-in flag as it is. Brew reads opt-outs only from a
Subscribedcolumn, so any other column becomes a plain custom property and those people import as subscribed. - New columns become camelCase properties. A
Plan Typecolumn becomesplanType. 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, andYYYY-MM-DDneeds no choice. See date formats.
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 keepsSubscribed 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.
Bring Your Consent Records
A contact’sSubscribed 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/contactstakes aconsentrecord per contact, with the original date ascapturedAt. It never changesSubscribed.
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 asplan 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 runspostmark 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 theTemplateModel 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 athttps://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:
Tobecomes the payload’semail. That’s the contact the automation runs for.TemplateModelbecomes the rest of thepayload, read in the email as{{ trigger.order.total }}.TemplateAlias,From, andMessageStreamleave 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.
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:- Rebuild while Postmark keeps sending. Import contacts and rebuild audiences, templates, and automations. None of it touches live sending. Keep every Brew automation unpublished.
- Check your segments. Confirm each Audience’s count against the query it replaces. See Step 4.
- 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.
- 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.
- 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.
- 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.
- 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.
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:- Self-Service Tools
- Talk to Our Team
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.