Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Postmark Laravel Package

jeffersongoncalves/laravel-postmark

Laravel Postmark integration that connects Postmark's transactional email API to your Laravel app. Provides a mail transport and simple configuration so you can swap your default mailer for Postmark and send reliable, deliverable email with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer, publish the config, and put a Postmark server token in your .env:

composer require <vendor>/<package>
php artisan vendor:publish --tag=postmark-config
POSTMARK_TOKEN=your-server-token

Where to look first, in order:

  1. config/postmark.php — token source, default message stream, timeout, and any base-URL override.
  2. The service provider — is the client bound as a singleton, and under what interface? That determines whether you inject it or use a facade.
  3. The client class — it exposes the whole surface (sendEmail, sendEmailWithTemplate, sendBatch, sendBatchWithTemplates, plus templates/bounces/messages/stats/server/suppressions groups).

First use case: send one transactional email and confirm it left the building.

$client->sendEmail([
    'From' => '[email protected]',
    'To' => $user->email,
    'Subject' => 'Welcome',
    'HtmlBody' => view('emails.welcome', ['user' => $user])->render(),
    'TextBody' => view('emails.welcome-text', ['user' => $user])->render(),
    'MessageStream' => 'outbound',
]);

Then verify with stats()->overview() or messages()->search() before writing another line — the fastest way to catch a wrong token, wrong stream, or an unverified sender signature is to look at the API's own view of your traffic.

Migrating from Postmark's own SDK or from SMTP? Do one real send through each code path (plain body, template, batch) and diff the resulting message details in Postmark's UI. Payload shape is the usual source of quiet breakage.

Implementation Patterns

Inject the client, don't reach for the facade. Bind it into constructors of a small Mailer/Notifier service and keep all payload construction there. This keeps PostmarkException handling in one place and makes the package swappable when you need to test.

Blade for HTML, templates for copy that changes without a deploy. sendEmail with rendered Blade is right for app-generated content; sendEmailWithTemplate with TemplateAlias + TemplateModel is right for onboarding drips, receipts, and anything a non-engineer edits. Prefer aliases over numeric TemplateId so the same code works against staging and production servers.

$client->sendEmailWithTemplate([
    'From' => '[email protected]',
    'To' => $invoice->user->email,
    'TemplateAlias' => 'invoice-ready',
    'TemplateModel' => ['name' => $invoice->user->name, 'total' => $invoice->formattedTotal],
    'MessageStream' => 'outbound',
]);

Batch deliberately, not casually. sendBatch/sendBatchWithTemplates take an array of message payloads and return per-message results. Chunk at a few hundred per request, and treat the response as a collection of outcomes rather than a success flag — see the gotchas.

Template management as a deploy step. The create/update/delete endpoints let you treat templates as code: an artisan command or a small DeployTemplates action that pushes layouts from version-controlled JSON on release. Create-if-missing by alias, then update. Never delete-then-create in the same run — you'll get a window where sends fail.

Bounce and suppression hygiene as a scheduled job. Use bounces()->list() with filters to pull recent bounces, bounces()->activate() to reactivate addresses you've verified, and bounces()->deliveryStats() to watch deliverability trend. Reserve bounces()->dump() for migrations and archival — see below.

Wrap sends in a domain-level guard. Before calling the client, ask your own database whether the recipient is mailable. Relying on a 422 from Postmark as your suppression mechanism means you're paying a network round trip and burning rate limit to learn something you already knew.

Testing. Because the client is almost certainly built on Laravel's HTTP client, Http::fake() on the Postmark base URL is the lightest test harness; assert on the request body for payload shape and fabricate per-message error responses to exercise the failure branches. For higher-level tests, bind a fake client implementation into the container instead.

Failure handling. Catch PostmarkException, branch on ErrorCode (a stable integer), log the error body alongside it, and map a handful of codes to user-facing behaviour: inactive/bounced recipient → mark suppressed and skip; invalid template model → alert (it's a code bug); auth failure → fail loudly, never silently swallow.

Gotchas and Tips

sendBatch is not atomic. Partial success is normal: some messages go out, others come back with their own error entries. If you treat a non-throwing response as "all sent," you will lose emails. Inspect each returned entry and reconcile against what you enqueued — the ordering is your only correlation unless you stamp each payload with Metadata you can match back.

ErrorCode is the contract, the message string is not. Match on the integer, log the body. Postmark's prose changes; the codes are far more stable. Don't parse error text into control flow.

Inactive-recipient (422) is a policy decision, not an error. Hard-bounced and spam-complained addresses get rejected. Decide up front whether that marks the user suppressed in your DB, queues a retry after reactivation, or just logs. Doing this ad hoc in each caller produces inconsistent behaviour.

Server token vs. account token. Most of the surface here is server-scoped and needs a server token. Account-level operations are not what this release is aimed at. If you get a confusing 401 on an endpoint that works elsewhere, check which token you're actually resolving — and make sure the per-environment token binding isn't silently falling back to a default.

Suppressions are per message stream. Suppressing on broadcast does not suppress on outbound. If you run multiple streams, your own suppression table needs a stream column or you'll over- or under-suppress. Same applies to bounces()->activate(): reactivation is scoped to a stream.

bounces()->dump() returns the whole set. It's an archival/migration tool, not a paginated query. Don't call it from a web request or a per-minute cron; it will be slow and large. Use bounces()->list() with filters for anything interactive.

Pagination is manual. The list endpoints take count/offset-style parameters; there's no lazy collection handed to you. Write one eachBounce()-style helper that loops until a short page and reuse it everywhere, otherwise you'll quietly only ever process the first page.

Batch size and rate limits. Keep batches well under the documented per-request cap and add a small sleep between chunks in bulk backfills. Rate limiting shows up as 429s that are indistinguishable from real failures if you're not reading ErrorCode.

Retries can duplicate mail. A timeout on sendEmail does not mean the message wasn't accepted. Blind retry logic generates duplicate receipts. Retry only on transport-level errors you're confident happened before the request was accepted, and let a human resolve the ambiguous case.

Template model keys are strict. Every {{placeholder}} in the template must have a corresponding key in TemplateModel, and a missing key renders empty rather than erroring — which means broken emails that look fine. Assert your model shape against the template in a test.

Config quirks in multi-tenant apps. A published config file caches its values. If you're resolving tokens per tenant at runtime, do it by constructing/binding clients rather than mutating config after boot — config() writes don't survive a second request under Octane or config caching.

Extension points worth knowing. The client binding is the seam: decorate it to add logging, metrics, and your own suppression guard around every send, and swap it in tests. If the package ships a facade, resist macro-ing business logic onto it — you'll lose the ability to intercept sends later.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle