Tutorial
Send email with Laravel
Send Laravel mail through Emailit with the emailit/emailit-laravel mail transport and facade, or plain SMTP, and verify Emailit webhooks.
This guide shows two ways to send Laravel mail through Emailit: the emailit/emailit-laravel package, which adds an emailit mail transport and an Emailit facade, and plain SMTP with Laravel’s built-in mailer. It ends with a webhook route that verifies signatures.
Prerequisites
- PHP 8.1 or later and Laravel 10, 11 or 12.
- A verified sending domain, for example
acme.com. - An API key. A Sending Only key restricted to your domain is enough.
- Until your workspace has production access, you can only send to the account emails of workspace members.
Option 1: the Emailit Laravel package
The package sends your existing Mailables, Markdown mailables, notifications and queued mail through the Emailit API, with no changes to your mail code.
Install the package
composer require emailit/emailit-laravelThe service provider is auto-discovered.
Configure the transport
Add your key and make Emailit the default mailer in .env:
EMAILIT_API_KEY=secret_••••••••••••••••••••••••••••••••
MAIL_MAILER=emailit
MAIL_FROM_ADDRESS=hello@acme.com
MAIL_FROM_NAME="Acme"Register the mailer in config/mail.php:
'mailers' => [
// ...
'emailit' => [
'transport' => 'emailit',
],
],MAIL_FROM_ADDRESS must be on a verified sending domain. To change the API base URL, publish the config file with php artisan vendor:publish --tag=emailit-config; you don’t need to for normal use.
Send a Mailable
Create a Mailable as usual:
php artisan make:mail WelcomeEmailnamespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class WelcomeEmail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(public User $user) {}
public function envelope(): Envelope
{
return new Envelope(subject: 'Welcome to Acme');
}
public function content(): Content
{
return new Content(view: 'emails.welcome');
}
}Send it, or queue it so the request doesn’t wait for the API:
use App\Mail\WelcomeEmail;
use Illuminate\Support\Facades\Mail;
Mail::to($user)->send(new WelcomeEmail($user));
Mail::to($user)->queue(new WelcomeEmail($user));Use the facade for API features
The Emailit facade exposes the full PHP SDK, for features Laravel’s mailer doesn’t model, such as stored templates and scheduled sends:
use Emailit\Laravel\Facades\Emailit;
$email = Emailit::emails()->send([
'from' => 'Acme <hello@acme.com>',
'to' => $user->email,
'template' => 'welcome',
'variables' => ['first_name' => $user->first_name],
'scheduled_at' => 'tomorrow at 9am',
]);
$email->id; // em_…The facade also covers domains, contacts, audiences, suppressions, webhooks and the other resources. If you prefer dependency injection, type-hint Emailit\EmailitClient in a controller or job and Laravel resolves it with your configured key.
Catch typed exceptions to handle failures:
use Emailit\Exceptions\ApiErrorException;
use Emailit\Exceptions\RateLimitException;
try {
Emailit::emails()->send($payload);
} catch (RateLimitException $e) {
// 429: release the job back to the queue and try again later
} catch (ApiErrorException $e) {
report($e); // $e->getHttpStatus() has the status code
}Option 2: plain SMTP
If you’d rather not add a package, point Laravel’s SMTP mailer at the Emailit relay:
MAIL_MAILER=smtp
MAIL_HOST=smtp.emailit.com
MAIL_PORT=587
MAIL_USERNAME=emailit
MAIL_PASSWORD=secret_••••••••••••••••••••••••••••••••
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=hello@acme.com
MAIL_FROM_NAME="Acme"On port 587 Laravel’s mailer upgrades the connection with STARTTLS. Older config/mail.php files read MAIL_ENCRYPTION and newer ones ignore it, so it’s safe to keep. For implicit TLS, use port 465; if your host blocks 587, use 2525 or 2587. Mailables, notifications and queues work exactly as with the package. See SMTP settings.
Over SMTP you can’t use stored templates or scheduled_at; use the facade or the API for those.
Receive webhooks
Create a webhook that points to https://your-app.com/webhooks/emailit, then store its signing secret:
EMAILIT_WEBHOOK_SECRET=whsec_••••••••'emailit' => [
'webhook_secret' => env('EMAILIT_WEBHOOK_SECRET'),
],Add a controller that checks the signature against the raw body:
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class EmailitWebhookController extends Controller
{
public function __invoke(Request $request)
{
$payload = $request->getContent();
$signature = (string) $request->header('X-Emailit-Signature');
$timestamp = (string) $request->header('X-Emailit-Timestamp');
$expected = hash_hmac('sha256', $timestamp.'.'.$payload, config('services.emailit.webhook_secret'));
if (abs(time() - (int) $timestamp) > 300 || ! hash_equals($expected, $signature)) {
abort(401, 'Invalid signature');
}
// The body is a JSON array of up to 100 events.
foreach (json_decode($payload, true) as $event) {
match ($event['type']) {
'email.bounced', 'email.complained' => $this->stopEmailing($event['data']['object']['to']),
default => null,
};
}
return response()->noContent();
}
private function stopEmailing(string $address): void
{
// Mark the address as undeliverable in your database.
}
}Register the route and exclude it from CSRF protection, because Emailit can’t send a CSRF token:
use App\Http\Controllers\EmailitWebhookController;
Route::post('/webhooks/emailit', EmailitWebhookController::class);->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: ['webhooks/emailit']);
})On Laravel 10, add 'webhooks/emailit' to the $except array in app/Http/Middleware/VerifyCsrfToken.php instead. Return a 2xx within 30 seconds, and push slow work onto a queue. See Request signature.
Production tips
- Queue your mail. Use
queue()orShouldQueueso web requests don’t wait on email, and throttle bulk jobs (for example withRedis::throttle) to stay under your sending limits. New workspaces can send 2 emails per second by default. - Cache config safely. After
php artisan config:cache,env()only works inside config files. Read the key through config, as the package does. - Use a dedicated key. Give each app and environment its own Sending Only key so you can rotate one without touching the others. See API keys.
- Deduplicate webhooks. Store each
event_idyou process and skip repeats, because failed deliveries are retried.