How to send email in Laravel
Configure Laravel's mail system to send through a dedicated provider, queue it properly, and handle failures — using the mailer you already have.
Laravel's mail system is one of the best parts of the framework, and it needs almost no changes to send through a dedicated provider. The work is in the parts people skip: queueing, failure handling and the from address.
Before you start
You need three things, and they take about five minutes:
- An account. Sign up free — 5,000 emails a month, no card.
- A verified sending domain. Add your domain in the dashboard and publish the DKIM and SPF records it gives you. You cannot send from a domain you have not proved you control, and neither can anyone pretending to be you — that is the whole point. If the terms are unfamiliar, SPF, DKIM and DMARC explained covers them properly.
- An API key. Create one under API Keys. Treat it like a password: it can send mail as you.
Put the key in your environment rather than your source. Laravel reads it from there in every example below.
Configuration
Laravel reads mail settings from the environment. Point the SMTP mailer at the relay:
MAIL_MAILER=smtp
MAIL_HOST=smtp.pigeonify.com
MAIL_PORT=587
MAIL_USERNAME=your-smtp-username
MAIL_PASSWORD=your-smtp-password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=you@yourdomain.com
MAIL_FROM_NAME="${APP_NAME}"
Every Mail::send and every notification in the application now goes through it. Nothing else changes.
A mailable that queues itself
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class WelcomeEmail extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
public function __construct(public string $name) {}
public function envelope(): Envelope
{
return new Envelope(subject: 'Welcome aboard');
}
public function content(): Content
{
return new Content(markdown: 'emails.welcome');
}
}
implements ShouldQueue is the important line. Without it Laravel sends inside the request, and your signup endpoint is as slow as your mail provider on its worst day.
Mail::to($user->email)->send(new WelcomeEmail($user->name));
Despite the name, send queues it when the mailable implements ShouldQueue.
Handling failures
A queued mailable that throws is retried by the queue worker and then lands in failed_jobs. That table is not a notification — nobody reads it unless something makes them:
public function failed(\Throwable $exception): void
{
Log::error('Welcome email failed', [
'name' => $this->name,
'error' => $exception->getMessage(),
]);
}
Set sensible retry bounds, too. Three attempts with backoff is usually right; the default of retrying forever is not:
public $tries = 3;
public $backoff = [10, 60, 300];
Using the API instead of SMTP
If you would rather not run SMTP, wrap the API in a small service and call it from a job:
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class Mailer
{
public function send(string $to, string $subject, string $html): string
{
$response = Http::withHeaders(['X-API-Key' => config('services.pigeonify.key')])
->timeout(10)
->retry(3, 200, throw: false)
->post('https://www.pigeonify.com/api/v1/send', [
'from' => config('mail.from.address'),
'to' => $to,
'subject' => $subject,
'html' => $html,
]);
$response->throw();
return $response->json('id');
}
}
Laravel's HTTP client has retry built in, and throw: false on the retry means you decide what to do with a 4xx rather than retrying it.
Testing without sending
Mail::fake();
Mail::assertQueued(WelcomeEmail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
Use assertQueued rather than assertSent for queued mailables — assertSent passes only for mail sent synchronously, and the mismatch produces a confusing failure.
What to get right before production
Do not send from a request handler and wait. An HTTP call to any email provider can take a second or more, and a slow provider becomes a slow checkout page. Queue the send and return.
Handle the failure. A send can fail because the address is wrong, because you are rate limited, or because the provider is having a bad minute. The first is permanent and the other two are not — retrying a dead address forever is how a sender's reputation degrades. See how to handle bounces.
Use a subdomain. Send transactional mail from mail.yourdomain.com rather than the domain that carries your business correspondence. If sending reputation is ever damaged, the damage stays contained.
Never send to an address that has bounced or unsubscribed. Suppression should be automatic, not a thing you remember. Pigeonify suppresses permanent bounces and complaints for you and refuses to send to them afterwards.
Where to go next
- Laravel API reference — every parameter, including attachments and custom headers
- Transactional vs marketing email — why they need different handling
- Why emails land in spam — the things that actually cause it
If you are weighing providers, we publish honest comparisons that say where the other product is better.