# 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](/docs/domains/add-a-domain/), for example `acme.com`.
- An [API key](/docs/developers/api-keys/). A Sending Only key restricted to your domain is enough.
- Until your workspace has [production access](/docs/workspaces/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

```bash
composer require emailit/emailit-laravel
```

The service provider is auto-discovered.

### Configure the transport

Add your key and make Emailit the default mailer in `.env`:

```bash title=".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`:

```php title="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:

```bash
php artisan make:mail WelcomeEmail
```

```php title="app/Mail/WelcomeEmail.php"
namespace 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:

```php
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](/docs/frameworks/php/), for features Laravel's mailer doesn't model, such as stored templates and scheduled sends:

```php
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:

```php
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:

```bash title=".env"
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](/docs/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](/docs/webhooks/set-up/) that points to `https://your-app.com/webhooks/emailit`, then store its signing secret:

```bash title=".env"
EMAILIT_WEBHOOK_SECRET=whsec_••••••••
```

```php title="config/services.php"
'emailit' => [
    'webhook_secret' => env('EMAILIT_WEBHOOK_SECRET'),
],
```

Add a controller that checks the signature against the raw body:

```php title="app/Http/Controllers/EmailitWebhookController.php"
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:

```php title="routes/web.php"
use App\Http\Controllers\EmailitWebhookController;

Route::post('/webhooks/emailit', EmailitWebhookController::class);
```

```php title="bootstrap/app.php (Laravel 11 and 12)"
->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](/docs/webhooks/request-signature/).

## Production tips

- **Queue your mail.** Use `queue()` or `ShouldQueue` so web requests don't wait on email, and throttle bulk jobs (for example with `Redis::throttle`) to stay under your [sending limits](/docs/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](/docs/developers/api-keys/).
- **Deduplicate webhooks.** Store each `event_id` you process and skip repeats, because failed deliveries are retried.

## Next steps

  - [PHP SDK guide](/docs/frameworks/php/): The client behind the facade.
  - [Templates](/docs/templates/): Design emails in Emailit and send them by alias.
  - [Webhook event types](/docs/webhooks/event-types/): Every event and its payload.
  - [SMTP troubleshooting](/docs/smtp/troubleshooting/): Fix authentication and connection errors.

---
Source: https://emailit.com/docs/frameworks/laravel/
