mailersend/laravel-driver
Official MailerSend mail driver for Laravel. Send transactional email through MailerSend’s API using Laravel’s native Mail facade, with a drop-in transport that supports templates, attachments, tags, and webhooks. Simple configuration and reliable delivery for Laravel apps.
Install and wire it up in four steps.
composer require mailersend/laravel-driver
MAIL_MAILER=mailersend
MAILERSEND_API_KEY=mlsn.xxxxxxxxxxxxxxxx
[email protected]
MAIL_FROM_NAME="Your App"
// config/mail.php
'mailersend' => [
'transport' => 'mailersend',
'api_key' => env('MAILERSEND_API_KEY'), // driver reads the key from the mailer config
],
php artisan config:clear # or config:cache in deploy
The service provider is auto-discovered; the transport is registered under the mailersend key, so anything that resolves a mailer (Mail::, Mailable, Notification, MailMessage) can use it.
Where to look first
src/MailerSendServiceProvider.php and src/MailerSendTransport.php — the transport is thin; read it once and you know exactly which Mailable features survive the conversion to an API payload (cc/bcc/reply-to/attachments/headers).MSxxxxx codes.First use case: smoke test through the real transport, not tinker.
Mail::mailer('mailersend')->raw('Test', function ($m) {
$m->to('[email protected]')->subject('Driver smoke test');
});
Confirm it appears in Activity before building Mailables on top. If the from-address domain isn't verified (SPF/DKIM in MailerSend), you'll get a 4xx here and nowhere else — better to find that on line one than in a queued job at 3am.
Default vs. explicit mailer. Set MAIL_MAILER=mailersend for the common path, then opt specific mailables/notifications in or out. Transactional (receipts, password resets) → mailersend. Internal notification noise → log or SMTP, so you don't burn API quota.
// Mailable
public function __construct(public Order $order) {}
public function build()
{
return $this->mailer('mailersend')
->subject("Order #{$this->order->id}")
->markdown('emails.order');
}
// Notification — define a mailer property on the class
class InvoicePaid extends Notification
{
public $mailer = 'mailersend';
}
Failover / routing. Laravel 9+ lets you stack mailers. Useful when you want SMTP as a safety net:
'default' => env('MAIL_MAILER', 'failover'),
'failover' => [
'transport' => 'failover',
'mailers' => ['mailersend', 'smtp'],
],
Verify this actually catches API errors — see the Gotchas section, because failover keys off Symfony's TransportExceptionInterface and an SDK exception may not implement it.
Queue everything. The driver is a synchronous HTTP call to MailerSend; a slow API turns a 200ms request into a 2s one. Combine ShouldQueue with real retry semantics:
class SendReceipt extends Mailable implements ShouldQueue
{
public $tries = 5;
public $backoff = [10, 60, 300, 900]; // 429s and 5xx deserve patience
public $maxExceptions = 3;
}
Attachments. attach(), attachFromStorage(), and attachData() all work — they're streamed/encoded at send time, so files are read inside the queue worker, not when the job is dispatched. Pass storage paths and IDs into the job, not raw file contents.
Multi-tenant / multi-domain. Two clean options:
// A) named mailers, one per sending domain/API key
'amazon' => ['transport' => 'mailersend', 'api_key' => env('MS_KEY_A')],
'apple' => ['transport' => 'mailersend', 'api_key' => env('MS_KEY_B')],
// then ->mailer($tenant->mailer_name)
Option B — mutating config() at runtime — works but is a footgun under Octane and long-lived queue workers (see Gotchas). Prefer named mailers.
Local dev and tests. Keep MAIL_MAILER=log or Mailpit locally; point staging at MailerSend's trial domain (which only delivers to your own account email — do not try to demo to a client with it). In feature tests, Mail::fake() intercepts before the transport, so no API key or network is needed — assert on Mail::assertSent() as usual.
Beyond mail. Delivery events (delivered, opened, bounced, spam complaint) come via MailerSend webhooks, not this driver. Add a webhook route and verify the signature with the signing secret; store events against your message/recipient records. For templates, lists, inbound routing, or the bulk endpoint, use mailersend/mailersend directly alongside the driver — they coexist fine.
Mail::send() returning without an exception means "queued at MailerSend," not "delivered." Don't build retry logic around the return value; build it around webhooks.failover transport only switches on TransportExceptionInterface. If the driver lets a MailerSend*Exception escape unwrapped, a hard API failure won't trigger failover. Test it deliberately (bad API key) before trusting it in production; otherwise catch and rethrow in your own job layer.from address must match. Mismatched MAIL_FROM_ADDRESS between .env and MailerSend produces a 4xx that reads like an auth problem.php artisan config:clear locally, config:cache in deploy, and restart queue workers and Octane — long-running processes keep the old config in memory.Mail::queueing 10k individual sends.X-Custom-Thing on a Mailable may be dropped or rejected by the API. Stick to what the transport maps (reply-to, cc/bcc, tags via the documented mechanism) and verify in Activity.Mail::extend('mailersend', fn ($config) => new YourTransport(...)) if you need a custom HTTP client, timeouts, or structured logging around the SDK. Useful for injecting request IDs into logs so MailerSend Activity entries are traceable back to a Laravel job.config('mail.mailers.mailersend'), (2) dashboard Activity for the recipient + timestamp, (3) API error code, (4) failed_jobs payload. Ninety percent of issues are resolved at step 2.How can I help you explore Laravel packages today?