Search by

getoutbox / outbox-laravel

Yusadolat

Outbox Stack mail driver and webhooks for Laravel

v0.1.0 2026-10-06 17:16 UTC

This package is auto-updated.

Last update: 2026-10-06 17:23:06 UTC


README

Outbox Stack mail driver and webhooks for Laravel 11, 12 and 13. Your existing Mailables and notifications work unchanged.

composer require getoutbox/outbox-laravel

Send mail

Add the mailer to config/mail.php:

'mailers' => [
    'outbox' => ['transport' => 'outbox'],
    // ...
],

Then in .env (use a Sending only key from Developers in the dashboard):

MAIL_MAILER=outbox
OUTBOX_API_KEY=mk_...
MAIL_FROM_ADDRESS=hello@your-verified-domain.com

That's it: Mail::to($user)->send(new InvoicePaid($invoice)) now goes through Outbox Stack. Supported from a Mailable:

Laravel Outbox Stack
to, cc, bcc with names One message per recipient, each with its own id and bounce tracking (50 recipients per mail)
replyTo replyTo (first address)
attachments(), attachData() Attachments (10 files, 10 MB)
$message->embed() in views Inline images
->metadata('order_id', 42) Metadata, returned in webhooks
->withSymfonyMessage(fn ($m) => $m->getHeaders()->addTextHeader('X-Entity-Ref', '...')) Custom headers
X-Outbox-Idempotency-Key header Idempotency key (not sent as a header)

->tag() is ignored; use ->metadata(). The API's message id is available as $sentMessage->getMessageId() and in the X-Outbox-Message-Id header. API errors throw Symfony's TransportException with the error code and request id in the message.

Make queued mail safe to retry by giving each Mailable a stable key:

public function headers(): Headers
{
    return new Headers(text: ['X-Outbox-Idempotency-Key' => "invoice-paid:{$this->invoice->id}"]);
}

Webhooks

Add a webhook in Developers pointing at https://your-app.com/outbox/webhook and set its secret:

OUTBOX_WEBHOOK_SECRET=whsec_...

The package registers POST /outbox/webhook (only when the secret is set), rejects bad signatures with 400, and dispatches events:

use Outbox\Laravel\Events\MessageBounced;

Event::listen(function (MessageBounced $event) {
    User::where('email', $event->email())->update(['email_bounced_at' => now()]);
    $orderId = $event->metadata()['order_id'] ?? null;
});

Events: MessageSent, MessageDelivered, MessageBounced, MessageComplained, MessageFailed, MessageOpened, MessageClicked, ContactSubscribed, ContactUnsubscribed, plus WebhookReceived for every event. Each has type(), data(), messageId(), email(), metadata() and the raw payload. The route has no CSRF or session middleware; requests are authenticated by signature.

Using the API directly

use Outbox\Laravel\Facades\Outbox;

Outbox::send(['from' => 'app@your-verified-domain.com', 'to' => 'ada@example.com', 'subject' => 'Hi', 'text' => 'Hello']);
Outbox::getMessage($id);

Configuration

php artisan vendor:publish --tag=outbox-config publishes config/outbox.php: OUTBOX_BASE_URL, OUTBOX_TIMEOUT, OUTBOX_MAX_RETRIES, OUTBOX_WEBHOOK_PATH and webhook middleware. A mailer entry can carry its own api_key to send from another workspace.

Development

composer install
composer test
composer analyse