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

Lob Laravel Package

laravel-notification-channels/lob

Laravel Notification Channels Lob adds a notification channel for sending physical mail such as postcards and letters through Lob's API. Define a Lob message in your notification, configure your API key, and deliver mail using Laravel's built-in notification system.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require laravel-notification-channels/lob
php artisan vendor:publish --provider="NotificationChannels\Lob\LobServiceProvider"

Set the key in .env (the config file reads LOB_SECRET_KEY; use a test_ prefixed key until you are ready to spend money):

LOB_SECRET_KEY=test_xxxxxxxxxxxxxxxxxxxx

Where to look first. Read src/LobChannel.php and src/LobMessage.php before the README. The README is a few years old and the message object is the real API surface — the available setters there tell you exactly what the package supports (it is deliberately thin). Also check composer.json for the illuminate/support and lob/lob-php constraints, because those determine whether you need a fork.

Address the notifiable. The channel resolves a destination via the standard Laravel route mechanism, so your notifiable needs one of:

public function routeNotificationForLob()
{
    return $this->lob_address_id; // address ID already registered in your Lob account
}

or, for one-off sends, an on-demand route:

Notification::route('lob', $addressId)->notify(new OrderPostcard($order));

First use case — a transactional postcard. The classic fit is a thank-you or re-order card triggered by a domain event:

class OrderPostcard extends Notification implements ShouldQueue
{
    public function __construct(public Order $order) {}

    public function via($notifiable): array
    {
        return [LobChannel::class];
    }

    public function toLob($notifiable): LobMessage
    {
        return (new LobMessage)
            ->from(config('services.lob.from_address_id'))
            ->front('https://cdn.example.com/cards/order-'.$this->order->id.'-front.html')
            ->back('https://cdn.example.com/cards/order-'.$this->order->id.'-back.html')
            ->description("Order #{$this->order->id} thank-you card");
    }
}

That single notification is enough to prove the integration end-to-end against Lob's test key.

Implementation Patterns

Keep an address book, not raw addresses. Lob wants address IDs (adr_...). Create the address once via the Lob API when a customer first qualifies for mail, persist lob_address_id on your users or addresses table, and reuse it. Recreating on every send produces duplicate records in the Lob dashboard and makes reconciliation painful. Wrap creation in a small service so you can backfill existing users with an artisan command.

Render HTML to a reachable URL. Lob fetches the artwork server-side, so the URL must be public without auth. Two workable patterns:

  • A signed route (URL::temporarySignedRoute) with an expiry comfortably longer than Lob's fetch latency — do not use 5-minute expiries.
  • Preheat: render the Blade view to S3/R2 at order time, store the URL, and pass the static asset URL to front()/back(). This is more reliable and gives you an immutable artifact per send.

Never point Lob at an asset behind a login or a localhost URL — it will fail validation with an opaque 422.

Always queue, and combine with on-demand routes. ShouldQueue plus afterCommit() is the right default. Physical mail is asynchronous by nature, so there is no reason to block a request:

Notification::route('lob', $notifiable->lob_address_id)
    ->notify((new OrderPostcard($order))->afterCommit());

Capture the Lob resource ID. Laravel discards whatever the channel's send() returns, so notify() gives you nothing. If you need the psc_... ID for support and reconciliation, either:

  • listen for Illuminate\Notifications\Events\NotificationSent and read $event->response, or
  • write a thin wrapper channel that subclasses LobChannel, calls parent::send(), and dispatches your own event with the result.

The first is less code; the second is more explicit and easier to test.

Testing. Two layers, both cheap:

Notification::fake();

Notification::route('lob', 'adr_test_123')
    ->notify(new OrderPostcard($order));

Notification::assertSentOnDemand(OrderPostcard::class, function ($n, $channels, $notifiable) {
    return $notifiable->routes['lob'] === 'adr_test_123';
});

For a real channel test, bind a mock into the container — LobChannel receives Lob\Lob via constructor injection, so $this->mock(Lob::class) or $this->instance(Lob::class, $fake) intercepts the HTTP call without touching the package.

Extending the channel. The package realistically covers postcards. Letters and checks are different Lob endpoints with different payloads. The cleanest extension is a sibling LobLetterChannel + LobLetterMessage in your app/Notifications/Channels directory reusing the same injected client, rather than bending LobMessage with conditionals.

Gotchas and Tips

The package is stale. Last release was March 2020 and it pins an old lob/lob-php. Lob's API has moved on (versioned via a Lob-Version header), and Laravel has moved from 7 to 11. Expect to fork: bump illuminate/support, bump or replace the SDK, and keep the fork in a VCS repository entry in composer.json. Budget an afternoon for this before promising the feature to anyone.

No webhook support at all. This package is fire-and-forget. Delivery, return-to-sender, and "in transit" events all arrive via Lob webhooks that you must implement yourself: a controller that verifies the signature header, checks the event type, and updates your lob_postcard_id row. Until you build that, you have no idea whether the card was printed, delivered, or bounced.

Idempotency and retries — the expensive gotcha. A queued notification that times out after Lob accepted the request will retry and send a second physical postcard. Lob supports an Idempotency-Key header for exactly this; the bundled SDK almost certainly does not expose it. Mitigations: set $tries = 1 on the notification, or derive a deterministic key from the order ID and set it at the HTTP layer in your fork.

Guard the route method. If routeNotificationForLob() returns null (customer has no verified address, address was never registered), you get a generic exception from the channel rather than a clean skip. Check eligibility before notifying, and consider a shouldSend() method on the notification to short-circuit.

Test keys are per-environment. adr_... IDs created under a test_ key do not exist under a live key. When you flip to live, addresses must be recreated — plan a migration path rather than assuming the IDs carry over.

Config caching quirks. LOB_SECRET_KEY is read at boot through config('lob.secret_key'). After changing .env in a deployed environment, php artisan config:cache is required. A stale cached token surfaces as a generic 401, which is easy to misdiagnose as a bad key.

Defaults cost money differently. Postcard sizes are not free-equivalent: 4×6 is cheapest, 6×9 and 6×11 materially more, and mailType (usps_first_class vs usps_standard) changes both price and delivery window. Set size() and mailType() explicitly in toLob() rather than relying on Lob's defaults — otherwise a future Lob default change silently changes your unit economics.

Debugging path. Catch Lob\Exception\ValidationException in your own wrapper and log the response body — Lob's validation messages name the exact field (often the artwork URL or a missing address field). Also log the Lob request ID from the response; Lob support and the dashboard can trace a postcard from that ID alone.

Treat it as irreversible. Every send costs money and cannot be recalled. Gate the first production rollout behind a config flag, throttle per user (one card per customer per quarter is a sane default), and dry-run the whole pipeline against test keys before any live key exists in production.

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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo