log in
consulting hosting industries the daily tools about contact

Postmark: Mixing Transactional and Marketing Email Will Hurt You

I learned the hard way that sending newsletters and password resets from the same stream is a deliverability disaster. Here's what Postmark taught me.

A client's password reset emails were landing in spam. Not occasionally — consistently. We dug in and found the culprit immediately: their marketing newsletter and their transactional emails were going out through the same Postmark message stream. The newsletter had a 12% spam complaint rate that month. It poisoned everything.

This is a completely avoidable mistake, and I see it constantly.

What Postmark Actually Solves

Postmark is a transactional email provider. That's their whole pitch, and they mean it. They're not trying to be Mailchimp. They built their infrastructure specifically for emails that users need to receive — account confirmations, password resets, order receipts, two-factor auth codes, booking confirmations. The emails where "went to spam" means your product is broken.

The core value proposition is deliverability reputation. Postmark maintains separate IP pools and enforces real sending policies. They'll actually suspend accounts that abuse the platform. That sounds harsh until you realize it's exactly why your transactional mail gets delivered — they protect the shared reputation by kicking out the bad actors.

But here's where developers get themselves into trouble: Postmark also supports broadcast (marketing) streams now, and it's trivially easy to route everything through one stream because it's simpler to configure. Don't.

Message Streams: The Architecture You Need to Understand

Postmark separates email into Message Streams. Every server has at least one, and each stream has its own reputation, its own unsubscribe handling, and its own deliverability history. Streams come in two flavors:

  • Transactional — no unsubscribe header required, high-priority delivery, strict use policies
  • Broadcasts — built for bulk/marketing sends, includes list-unsubscribe headers, designed for opt-in audiences

The moment you send a promotional email through a transactional stream — a sale announcement, a newsletter, a "we miss you" re-engagement campaign — you're gambling with the deliverability of your password resets. Spam complaints on that promotional mail drag down the stream's reputation. ISPs see complaints and start filtering everything from that stream more aggressively. Now your 2FA codes are going to the promotions tab, or worse, spam.

Postmark enforces this separation, but it's opt-in. You have to wire it up correctly.

Setting It Up Right in Laravel

Laravel's mail system makes this straightforward once you understand what you're doing. You'll configure multiple Postmark "servers" or use message stream IDs to route mail correctly.

First, your .env and config/mail.php setup for multiple streams:

// .env
MAIL_MAILER=postmark
POSTMARK_TOKEN=your-transactional-token

POSTMARK_BROADCAST_TOKEN=your-broadcast-token
// config/mail.php
'mailers' => [
    'postmark' => [
        'transport' => 'postmark',
        'message_stream_id' => env('POSTMARK_TRANSACTIONAL_STREAM', 'outbound'),
    ],

    'postmark-broadcast' => [
        'transport' => 'postmark',
        'token' => env('POSTMARK_BROADCAST_TOKEN'),
        'message_stream_id' => env('POSTMARK_BROADCAST_STREAM', 'broadcast'),
    ],
],

If you're using a single Postmark server with multiple streams (which is fine for smaller setups), you can use the same token and just switch the message_stream_id. The stream ID is set in your Postmark dashboard when you create the stream.

Now in your Mailable classes, explicitly declare which mailer to use:

<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class PasswordResetMail extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(public string $resetUrl) {}

    public function envelope(): Envelope
    {
        return new Envelope(
            subject: 'Reset your password',
            mailer: 'postmark', // explicitly transactional
        );
    }

    public function content(): Content
    {
        return new Content(
            markdown: 'emails.password-reset',
        );
    }
}
<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class WeeklyNewsletterMail extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(public array $articles) {}

    public function envelope(): Envelope
    {
        return new Envelope(
            subject: 'This week from the blog',
            mailer: 'postmark-broadcast', // explicitly broadcast
        );
    }

    public function content(): Content
    {
        return new Content(
            markdown: 'emails.newsletter',
        );
    }
}

Don't rely on the default mailer being correct. Set it explicitly on every Mailable. Three months from now someone will change MAIL_MAILER in production for a reason that seemed good at the time, and your transactional mail will silently start going through the wrong stream.

The Gotchas That Will Bite You

Complaint rate thresholds are real and they act fast. Postmark will warn you, then suspend a stream, if complaint rates get out of hand. I've seen this happen to a client who imported an old list (people who hadn't opted in recently) and blasted a promotional email. Complaints spiked, Postmark flagged it within hours. The transactional stream was on a separate server in that case, so password resets kept working — but only because we had the separation in place.

Unsubscribe handling is different per stream type. Broadcast streams automatically include List-Unsubscribe headers and Postmark handles suppression for you. Transactional streams don't, and they shouldn't — you don't want a user "unsubscribing" from their account security emails. Make sure you're not trying to manage marketing unsubscribes through a transactional stream. It won't work the way you expect, and you'll have CAN-SPAM compliance problems.

The Postmark API for broadcast sends wants a MessageStream field. If you're calling the API directly instead of using Laravel's mail transport, you have to pass it explicitly:

$client = new \Postmark\PostmarkClient(config('services.postmark.token'));

$response = $client->sendEmail(
    from: 'hello@yourdomain.com',
    to: $recipient,
    subject: 'Your order shipped',
    htmlBody: $html,
    textBody: $text,
    messageStream: 'outbound' // 'outbound' is the default transactional stream name
);

Leave off messageStream and it defaults to outbound (transactional). That's fine for transactional. But if you're doing a broadcast send and you forget to set it to your broadcast stream ID, you're contaminating your transactional reputation. I've audited codebases where this was happening silently for months.

Bounce handling matters too. Hard bounces accumulate on a per-stream basis. A marketing campaign to a stale list that generates 8% hard bounces is going to hurt that stream's reputation. Another reason to keep them separate — your transactional stream stays clean even when a broadcast campaign hits a bunch of dead addresses.

Postmark's dashboard analytics are per-stream. This is actually a feature. Once you have proper separation, you get clean signal on your transactional delivery rates without marketing noise polluting the numbers. I can tell a client their order confirmation open rate with confidence because it's not mixed in with newsletter opens.

When I'd Reach for Postmark

Postmark is my default for any application where transactional email reliability is business-critical. Healthcare portals (appointment reminders, portal notifications), e-commerce order flow, anything with account security emails. I used it for a biotech client's LIMS notification system and it's been rock-solid for two years.

For pure marketing email at volume — newsletters, campaigns, drip sequences — I'd pair Postmark for transactional with something like Mailchimp, Klaviyo, or even Postmark's own broadcast streams depending on volume. The point is the separation, not necessarily the vendor.

I wouldn't reach for Postmark if the entire use case is marketing email and there's no transactional component. Postmark is not cheap compared to bulk email providers, and if you're not leveraging the deliverability reputation for time-sensitive transactional mail, you're paying a premium for something you don't need.

Also, if you need sophisticated marketing automation — segmentation, visual campaign builders, behavioral triggers — Postmark isn't trying to be that. Use the right tool.

Closing

The rule is simple: transactional and marketing email have different reputations, different legal requirements, and different failure modes, so they belong in different streams. Postmark makes this easy to do correctly and makes it painful to ignore if you're paying attention. Set up your streams deliberately, wire your Mailables explicitly, and don't let a newsletter campaign take down your password reset flow.

Need help shipping something like this? Get in touch.