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

Postmark Php Laravel Package

wildbit/postmark-php

wildbit/postmark-php is the official PHP client for Postmark, making it easy to send transactional and bulk email through Postmark’s API. It supports templates, attachments, batch sending, bounce and message APIs, with simple Composer installation and Laravel integration via a companion adapter.

Deep Wiki
Context7

Getting Started

  1. Install the package (official Postmark SDK, MIT, actively released through 2025):

    composer require wildbit/postmark-php
    
  2. Get a server token from Postmark → Server → API Tokens. This is different from the account token used for provisioning (see PostmarkAdminClient below). Put it in config/services.php as services.postmark.token and POSTMARK_TOKEN in .env.

  3. Instantiate Postmark\PostmarkClient and send your first message:

    use Postmark\PostmarkClient;
    
    $client = new PostmarkClient(config('services.postmark.token'));
    
    $response = $client->sendEmail(
        from: '[email protected]',
        to: '[email protected]',
        subject: 'Your receipt',
        htmlBody: view('emails.receipt', $data)->render(),
        textBody: '...',
        tag: 'receipt',
        messageStream: 'outbound',
    );
    
    $response->MessageID; // use this for support lookups
    
  4. Where to look first

    • src/Postmark/PostmarkClient.php — every server-level call (sendEmail, sendEmailBatch, sendEmailWithTemplate, getBounces, getOutboundMessages, getInboundMessages, …).
    • src/Postmark/PostmarkAdminClient.php — account-level: servers, domains, sender signatures, DKIM/SPF verification.
    • src/Postmark/Models/ — response shapes (PostmarkResponse, PostmarkBounce, PostmarkOutboundMessage, …).
    • src/Postmark/PostmarkException.php — the only exception you need to catch for API errors.
  5. Recommended first real use case: transactional sends (receipts, password resets). Once that works, layer templates + tags + MessageStream for broadcast, then bounces/message-search for support tooling.

Laravel note: the framework ships a postmark mail driver that wraps this same SDK. Use the driver for plain Mail::send, but reach for the SDK directly when you need bounces, message search, templates, inbound parsing, or admin/provisioning calls.

Implementation Patterns

Templated sends (recommended for anything user-facing). Store templates in Postmark, call by alias so you can promote changes without redeploying:

$client->sendEmailWithTemplate(
    from: '[email protected]',
    to: $user->email,
    templateIdOrAlias: 'welcome-email',
    templateModel: ['first_name' => $user->first_name, 'cta_url' => $url],
    tag: 'welcome',
    messageStream: 'outbound',
);

Batch sends. Build an array of payloads and send up to 500 per call. This is the right tool for digests, notifications, and backfills — it is dramatically faster than looping sendEmail:

$messages = $users->map(fn ($u) => [
    'From' => '[email protected]',
    'To' => $u->email,
    'TemplateAlias' => 'weekly-digest',
    'TemplateModel' => ['name' => $u->first_name, 'items' => $u->digest],
    'Tag' => 'digest',
    'MessageStream' => 'broadcast',
])->all();

foreach (array_chunk($messages, 500) as $chunk) {
    $results = $client->sendEmailBatch($chunk);
    foreach ($results as $i => $r) {
        if (($r->ErrorCode ?? 0) !== 0) {
            Log::warning('postmark batch item failed', ['index' => $i, 'error' => $r->ErrorCode, 'msg' => $r->Message]);
        }
    }
}

Attachments. Always go through PostmarkAttachment — it base64-encodes for the API. Do not pre-encode:

use Postmark\Models\PostmarkAttachment;

$msg = [
    'From' => '...', 'To' => '...',
    'Subject' => 'Invoice', 'TextBody' => '...',
    'Attachments' => [
        PostmarkAttachment::fromFile(storage_path("invoices/{$id}.pdf"), "invoice-{$id}.pdf", 'application/pdf'),
        // or:
        PostmarkAttachment::fromRawData($bytes, 'notes.txt', 'text/plain'),
    ],
];
$client->sendEmail(...$msg); // or sendEmailBatch([$msg])

Error handling wrapper. Centralize retries in a small service used across jobs:

use Postmark\PostmarkException;
use GuzzleHttp\Exception\ConnectException;

try {
    return $client->sendEmail(...$payload);
} catch (PostmarkException $e) {
    // 4xx → don't retry (bad address, unverified From, template error)
    // 429 or 5xx → retry with backoff
    if ($e->httpStatusCode === 429 || $e->httpStatusCode >= 500) {
        throw new \RuntimeException('postmark retryable', 0, $e);
    }
    report($e);
} catch (ConnectException $e) {
    // network/timeout — retry
    throw $e;
}

Bounce and suppression workflow. Run a scheduled job that pulls recent bounces and mirrors them into your own suppression list, then activateBounce() when an address is fixed:

$bounces = $client->getBounces(count: 500, offset: 0, type: 'HardBounce', inactive: false);

Support lookups. getOutboundMessages() with tag, recipient, fromDate/toDate, or messageID filters is your first stop for "did the customer get the email?". Pair with getOutboundMessageDetails() for SMTP dialogue and getMessageOpens()/getMessageClicks() for engagement.

Admin / provisioning. Use PostmarkAdminClient with the account token for domains, DKIM rotation, sender signatures. Cache the responses — you rarely need to hit this at runtime:

$admin = new \Postmark\PostmarkAdminClient(config('services.postmark.account_token'));
$admin->getDomains();

Inbound email. Handle Postmark's inbound webhook in a controller, return 200 fast, and queue processing:

$payload = json_decode($request->getContent(), true);
ProcessInboundEmail::dispatch($payload); // From, Subject, TextBody, HtmlBody, Attachments (base64)
return response()->noContent();

Testing. The client accepts a Guzzle client as the second constructor argument — inject a MockHandler (or a handler that returns canned Postmark JSON) so unit tests never hit the API. Avoid Http::fake(); it won't catch Guzzle traffic from this package.

$mock = new \GuzzleHttp\Handler\MockHandler([
    new \GuzzleHttp\Psr7\Response(200, [], json_encode(['MessageID' => 'abc', 'ErrorCode' => 0])),
]);
$client = new PostmarkClient('dummy', new \GuzzleHttp\Client(['handler' => $mock]));

Message streams. Explicitly pass MessageStream (outbound for transactional, broadcast for bulk). Set the correct default at the Postmark server level too — mixing streams will trigger rate-limit/broadcast compliance behavior you probably don't want.

Gotchas and Tips

  • $throw_errors defaults to true. The third constructor arg (new PostmarkClient($token, $client, false)) makes failures return error payloads instead of throwing. Don't mix modes across a codebase — silently swallowed errors become "why didn't the email send?" tickets.
  • Check ErrorCode, not just HTTP status, on batch responses. Batch calls return 200 even when individual items are rejected (invalid To, missing template variable, unverified sender). Iterate every item.
  • Guzzle version conflicts. The SDK pins a Guzzle major. If your app (or an old Laravel) is on Guzzle 6/7, run composer why-not wildbit/postmark-php before upgrading. This is the most common install-time surprise.
  • Sender identity. From must match a verified sender signature or an authenticated domain, otherwise 422 with errorCode: 300. Common in staging where the domain isn't verified.
  • Attachments. Use PostmarkAttachment::fromFile() / fromRawData() only. Pre-base64-encoding produces corrupt PDFs. Total message size limit is 10 MB.
  • Rate limits and 429. Postmark applies per-server throttling and a 500-message batch cap. Implement exponential backoff and split chunks; don't sleep(60) inside a request — queue the job.
  • Network errors are not PostmarkException. Timeouts and DNS failures surface as GuzzleHttp\Exception\ConnectException. Catch both, or you'll get unhandled exceptions that bypass your error-code logic.
  • Timeouts. Guzzle's default is no timeout for some setups; set an explicit timeout on the Guzzle client you pass in, especially in queue workers and cron.
  • PostmarkAdminClient uses the account token (different scope). Passing the server token yields 401.
  • TemplateModel type. Must be an object/associative array; a missing key referenced by the template returns 422 and blocks the send. Keep a smoke test per template.
  • TrackOpens / TrackLinks values are case-sensitive (HtmlAndText, HtmlOnly, TextOnly, None) and are gated by server-level settings.
  • Metadata limits. Keep metadata to a handful of short string key/values (~10 keys); oversized or non-string values are rejected.
  • Response shape history. Older versions returned plain arrays; newer ones return Postmark\Models\* objects. If you're upgrading across a major, audit ['MessageID'] vs ->MessageID access in your code.
  • Webhook security. Postmark inbound/bounce webhooks are not HMAC-signed out of the box. Use HTTP Basic Auth on the webhook URL, restrict by IP allowlist, and never trust the payload's From for authorization.
  • Inbound attachments arrive base64-encoded in the JSON. Decode before storage, and enforce your own size/type limit on top of Postmark's.
  • Reuse vs new clients. The client is cheap and stateless, but reusing one instance per worker (with a single Guzzle client) gives you connection keep-alive and lower overhead in batch jobs.
  • Open/click tracking is only meaningful if enabled at server level and if the recipient loads images/clicks — don't build hard business logic on opens alone.
  • Versioning note. The library is in maintenance mode; the release cadence is patch-level. For a heavy rewrite or brand-new integration, still use it (it's the official SDK), but plan for eventual migration if Postmark publishes a next-gen SDK.
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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums