Official Laravel integration for the Lettr email API.
- PHP 8.4+
- Laravel 10.x, 11.x, 12.x, or 13.x
composer require lettr/lettr-laravelPublish the configuration file:
php artisan vendor:publish --tag=lettr-configThe easiest way to set up Lettr in your Laravel application is using the interactive init command:
php artisan lettr:initThis command will guide you through:
- API Key Configuration - Automatically adds your Lettr API key to
.env - Mailer Setup - Configures the Lettr mailer in
config/mail.php - Template Download - Optionally pulls your email templates as Blade files
- Code Generation - Generates type-safe DTOs, Mailables, and template enums
- Domain Verification - Checks your sending domain is properly configured
Tip: If you already have a verified sending domain in your Lettr account, the init command will automatically configure your
MAIL_FROM_ADDRESSto match it.
After running lettr:init, you're ready to send emails:
use Illuminate\Support\Facades\Mail;
use App\Mail\Lettr\WelcomeEmail;
// Using a generated Mailable
Mail::to('user@example.com')->send(new WelcomeEmail($data));
// Or send templates inline
Mail::lettr()->to('user@example.com')->sendTemplate('welcome-email', substitutionData: $data);If you prefer to configure manually, add your Lettr API key to your .env file:
LETTR_API_KEY=your-api-keyTo send emails through Lettr, you must have a verified sending domain in your Lettr account. Your MAIL_FROM_ADDRESS (or any "from" address you use) must match a verified domain.
For example, if you've verified example.com in Lettr:
MAIL_FROM_ADDRESS=hello@example.com
MAIL_FROM_NAME="My App"Emails sent from addresses on unverified domains will be rejected.
Add the Lettr mailer to your config/mail.php:
'mailers' => [
// ... other mailers
'lettr' => [
'transport' => 'lettr',
],
],Set as default in .env:
MAIL_MAILER=lettrSend emails using Laravel's Mail facade:
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeEmail;
Mail::to('recipient@example.com')->send(new WelcomeEmail());use Lettr\Laravel\Facades\Lettr;
$response = Lettr::emails()->send(
Lettr::emails()->create()
->from('sender@example.com', 'Sender Name')
->to(['recipient@example.com'])
->subject('Hello from Lettr')
->html('<h1>Hello!</h1><p>This is a test email.</p>')
);
echo $response->requestId; // Request ID for tracking
echo $response->accepted; // Number of accepted recipientsuse Illuminate\Support\Facades\Mail;
use App\Mail\OrderConfirmation;
// Send using Mailable
Mail::to('customer@example.com')
->cc('sales@example.com')
->bcc('records@example.com')
->send(new OrderConfirmation($order));Mail::raw('Plain text email content', function ($message) {
$message->to('recipient@example.com')
->subject('Quick Update');
});Mail::send('emails.welcome', ['user' => $user], function ($message) {
$message->to('recipient@example.com')
->subject('Welcome!');
});Use Lettr for specific emails while keeping another default:
// Use Lettr for this specific email
Mail::mailer('lettr')
->to('recipient@example.com')
->send(new TransactionalEmail());
// Uses default mailer
Mail::to('other@example.com')
->send(new MarketingEmail());Instead of using Blade views, you can send emails using Lettr templates directly. Extend the LettrMailable class:
<?php
namespace App\Mail;
use Lettr\Laravel\Mail\LettrMailable;
use Illuminate\Mail\Mailables\Envelope;
class WelcomeEmail extends LettrMailable
{
public function __construct(
public string $userName,
public string $activationUrl,
) {}
public function build(): static
{
return $this
->template('welcome-email', version: 2)
->substitutionData([
'user_name' => $this->userName,
'activation_url' => $this->activationUrl,
]);
}
}Then send it like any other Mailable:
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeEmail;
Mail::to('user@example.com')
->send(new WelcomeEmail(
userName: 'John',
activationUrl: 'https://example.com/activate/abc123'
));| Method | Description |
|---|---|
template($slug, $version) |
Set template slug with optional version |
templateVersion($version) |
Set template version separately |
substitutionData($data) |
Set substitution variables for the template |
customHeaders($headers) |
Set custom email headers |
scheduledAt($when) |
Schedule delivery for a future DateTimeInterface (or ISO-8601 string) |
class OrderConfirmation extends LettrMailable
{
public function __construct(
public Order $order,
) {}
public function envelope(): Envelope
{
return new Envelope(
subject: "Order #{$this->order->id} Confirmed",
);
}
public function build(): static
{
return $this
->template('order-confirmation')
->substitutionData([
'order_id' => $this->order->id,
'customer_name' => $this->order->customer->name,
'items' => $this->order->items->map(fn ($item) => [
'name' => $item->name,
'quantity' => $item->quantity,
'price' => $item->formatted_price,
])->toArray(),
'total' => $this->order->formatted_total,
'shipping_address' => $this->order->shipping_address,
]);
}
}For quick template sending without creating a Mailable class, use the Mail::lettr() method:
Note: When no subject is provided, the template's own subject is used. Pass a
subjectonly if you want to override it.
use Illuminate\Support\Facades\Mail;
// Simple usage — subject comes from the template
Mail::lettr()
->to('user@example.com')
->sendTemplate('welcome-email', substitutionData: ['name' => 'John']);
// Override the template's subject
Mail::lettr()
->to('user@example.com')
->sendTemplate('welcome-email', subject: 'Hey John!', substitutionData: ['name' => 'John']);
// With specific template version
Mail::lettr()
->to('user@example.com')
->sendTemplate('order-confirmation', substitutionData: [
'order_id' => 123,
'items' => $items,
], version: 2);
// With a custom from address
Mail::lettr()
->from('hello@marketing.example.com', 'Marketing Team')
->to('user@example.com')
->sendTemplate('promo-campaign', substitutionData: $promoData);
// With CC and BCC
Mail::lettr()
->to('user@example.com')
->cc('manager@example.com')
->bcc('records@example.com')
->sendTemplate('invoice', substitutionData: $invoiceData);
// With a generated DTO (implements Arrayable)
Mail::lettr()
->to('user@example.com')
->sendTemplate('welcome-email', substitutionData: new WelcomeEmailData(
userName: 'John',
activationUrl: 'https://example.com/activate/abc123',
));By default, emails are sent from the address configured in MAIL_FROM_ADDRESS. To send from a different address (e.g. a marketing domain), use from():
// Inline template sending
Mail::lettr()
->from('hello@marketing.example.com', 'Marketing Team')
->to('user@example.com')
->sendTemplate('promo-campaign', substitutionData: $promoData);
// Regular Mailable sending
Mail::lettr()
->from('noreply@transactional.example.com')
->to('user@example.com')
->send(new OrderConfirmation($order));For Mailable classes, you can also set the from address in the envelope() method:
class MarketingEmail extends LettrMailable
{
public function envelope(): Envelope
{
return new Envelope(
from: new Address('hello@marketing.example.com', 'Marketing Team'),
subject: 'Special Offer',
);
}
}Note: The from address must belong to a verified sending domain in your Lettr account.
You can pass custom headers with your emails. These are forwarded directly to the Lettr API.
// Inline template sending
Mail::lettr()
->to('user@example.com')
->sendTemplate('welcome-email', substitutionData: ['name' => 'John'], customHeaders: [
'X-Campaign-Id' => 'welcome-2024',
'X-Entity-Ref' => 'order-123',
]);For Mailable classes, use the customHeaders() method:
class WelcomeEmail extends LettrMailable
{
public function build(): static
{
return $this
->template('welcome-email')
->customHeaders([
'X-Campaign-Id' => 'welcome-2024',
'X-Entity-Ref' => 'order-123',
]);
}
}use Lettr\Exceptions\ApiException;
use Lettr\Exceptions\TransporterException;
use Lettr\Exceptions\ValidationException;
use Lettr\Exceptions\NotFoundException;
use Lettr\Exceptions\UnauthorizedException;
use Lettr\Exceptions\RateLimitException;
use Lettr\Exceptions\QuotaExceededException;
try {
$response = Lettr::emails()->send($email);
} catch (RateLimitException $e) {
// Too many requests (429)
Log::warning("Rate limited, retry after: " . $e->retryAfter . "s");
} catch (QuotaExceededException $e) {
// Sending quota exceeded
Log::error("Quota exceeded: " . $e->getMessage());
} catch (ValidationException $e) {
// Invalid request data (422)
Log::error("Validation failed: " . $e->getMessage());
} catch (UnauthorizedException $e) {
// Invalid API key (401)
Log::error("Authentication failed: " . $e->getMessage());
} catch (NotFoundException $e) {
// Resource not found (404)
Log::error("Not found: " . $e->getMessage());
} catch (ApiException $e) {
// Other API errors
Log::error("API error ({$e->getCode()}): " . $e->getMessage());
} catch (TransporterException $e) {
// Network/transport errors
Log::error("Network error: " . $e->getMessage());
}The published config/lettr.php file contains:
return [
'api_key' => env('LETTR_API_KEY'),
'templates' => [
'html_path' => resource_path('templates/lettr'),
'blade_path' => resource_path('views/emails/lettr'),
'mailable_path' => app_path('Mail/Lettr'),
'mailable_namespace' => 'App\\Mail\\Lettr',
'dto_path' => app_path('Dto/Lettr'),
'dto_namespace' => 'App\\Dto\\Lettr',
'enum_path' => app_path('Enums'),
'enum_namespace' => 'App\\Enums',
'enum_class' => 'LettrTemplate',
],
];The templates block configures where lettr:pull, lettr:generate-dtos, and lettr:generate-enum commands save generated files.
The package also supports config('services.lettr.key') as a fallback for the API key.
Full guides, every facade method, and complete request/response details live in the docs:
📚 docs.lettr.com/quickstart/laravel
| Topic | Guide |
|---|---|
| Install, config, and verify | Installation |
| Mail facade, Lettr facade, Mailables, scheduling, testing | Sending Emails |
| Lettr templates, versioning, pull/push | Templates |
| Generated enums, DTOs, and Mailables | Type Safety |
| Add, verify, and manage sending domains | Domains |
| Webhook endpoints for delivery & engagement events | Webhooks |
| Lists, contacts, topics, properties, segments | Audience |
| List, send, and schedule campaigns | Campaigns |
| Endpoint reference (params & schemas) | API Reference |
Verify that your Lettr integration is correctly configured:
php artisan lettr:checkChecks mailer registration, API key validity, and sending domain verification. Returns exit code 0 if all checks pass.
Download email templates from your Lettr account as Blade files:
php artisan lettr:pull
php artisan lettr:pull --template=welcome-email
php artisan lettr:pull --as-html
php artisan lettr:pull --with-mailables
php artisan lettr:pull --dry-run| Option | Description |
|---|---|
--template= |
Pull only a specific template by slug |
--as-html |
Save as raw HTML instead of Blade |
--with-mailables |
Also generate Mailable and DTO classes |
--skip-templates |
Skip downloading templates, only generate DTOs and Mailables |
--dry-run |
Preview what would be downloaded |
Generate a PHP enum from your Lettr template slugs for type-safe template references:
php artisan lettr:generate-enum
php artisan lettr:generate-enum --dry-runGenerates an enum like:
enum LettrTemplate: string
{
case WelcomeEmail = 'welcome-email';
case OrderConfirmation = 'order-confirmation';
}Generate type-safe DTO classes from template merge tags:
php artisan lettr:generate-dtos
php artisan lettr:generate-dtos --template=welcome-email
php artisan lettr:generate-dtos --dry-runGenerated DTOs implement Arrayable and can be passed directly to sendTemplate():
$data = new WelcomeEmailData(userName: 'John', activationUrl: '...');
Mail::lettr()->to('user@example.com')->sendTemplate('welcome-email', substitutionData: $data);composer installcomposer lintcomposer analysecomposer testPlease see CONTRIBUTING for details.
MIT License. See LICENSE for details.