lettr / lettr-laravel
Lettr for Laravel - Official Laravel integration for Lettr email API
Requires
- php: ^8.4
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
- lettr/lettr-php: ^1.2.0
- symfony/mailer: ^6.2|^7.0
Requires (Dev)
- larastan/larastan: ^2.0|^3.0
- laravel/pint: ^1.18
- mockery/mockery: ^1.5
- orchestra/testbench: ^8.17|^9.0|^10.8
- pestphp/pest: ^2.0|^3.7
- dev-main / 1.x-dev
- v1.1.0
- v1.0.0
- v0.20.2
- v0.20.1
- v0.20.0
- v0.19.0
- v0.18.0
- v0.17.0
- v0.16.0
- v0.15.0
- v0.14.0
- v0.13.0
- v0.12.0
- v0.11.0
- v0.10.0
- v0.9.3
- v0.9.2
- v0.9.1
- v0.9.0
- v0.8.0
- v0.7.0
- v0.6.0
- v0.5.0
- v0.4.0
- v0.3.0
- v0.2.0
- v0.1.0
- dev-vojtech-readme-tests
- dev-vojtech-readme-fixes
- dev-vojtech-init-projects
- dev-vojtech-init-validate-apikey
This package is auto-updated.
Last update: 2026-03-12 09:16:40 UTC
README
Official Laravel integration for the Lettr email API.
Requirements
- PHP 8.4+
- Laravel 10.x, 11.x, or 12.x
Installation
composer require lettr/lettr-laravel
Publish the configuration file:
php artisan vendor:publish --tag=lettr-config
Getting Started
The easiest way to set up Lettr in your Laravel application is using the interactive init command:
php artisan lettr:init
This 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);
Manual Setup
If you prefer to configure manually, add your Lettr API key to your .env file:
LETTR_API_KEY=your-api-key
Sending Domain
To 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.
Quick Start
Using Laravel Mail (Recommended)
Add the Lettr mailer to your config/mail.php:
'mailers' => [ // ... other mailers 'lettr' => [ 'transport' => 'lettr', ], ],
Set as default in .env:
MAIL_MAILER=lettr
Send emails using Laravel's Mail facade:
use Illuminate\Support\Facades\Mail; use App\Mail\WelcomeEmail; Mail::to('recipient@example.com')->send(new WelcomeEmail());
Using the Lettr Facade Directly
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 recipients
Laravel Mail Integration
With Mailable Classes
use 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));
With Raw Content
Mail::raw('Plain text email content', function ($message) { $message->to('recipient@example.com') ->subject('Quick Update'); });
With Views
Mail::send('emails.welcome', ['user' => $user], function ($message) { $message->to('recipient@example.com') ->subject('Welcome!'); });
Multiple Mail Drivers
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());
Using Lettr Templates with Mailables
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' ));
LettrMailable Methods
| 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 |
Example: Order Confirmation
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, ]); } }
Inline Template Sending
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 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', ));
Testing with Mail::fake()
The Mail::lettr() method works seamlessly with Laravel's Mail::fake() for testing:
use Illuminate\Support\Facades\Mail; use Lettr\Laravel\Mail\InlineLettrMailable; public function test_welcome_email_is_sent(): void { Mail::fake(); // Trigger the code that sends the email Mail::lettr() ->to('user@example.com') ->sendTemplate('welcome-email', substitutionData: ['name' => 'John']); // Assert the email was sent Mail::assertSent(InlineLettrMailable::class, function ($mailable) { return $mailable->hasTo('user@example.com'); }); } public function test_order_confirmation_has_correct_recipients(): void { Mail::fake(); Mail::lettr() ->to('customer@example.com') ->cc('sales@example.com') ->bcc('records@example.com') ->sendTemplate('order-confirmation', substitutionData: ['order_id' => 123]); Mail::assertSent(InlineLettrMailable::class, function ($mailable) { return $mailable->hasTo('customer@example.com') && $mailable->hasCc('sales@example.com') && $mailable->hasBcc('records@example.com'); }); }
Direct API Usage
Sending Emails
Using the Email Builder (Recommended)
use Lettr\Laravel\Facades\Lettr; $response = Lettr::emails()->send( Lettr::emails()->create() ->from('sender@example.com', 'Sender Name') ->to(['recipient@example.com']) ->cc(['cc@example.com']) ->bcc(['bcc@example.com']) ->replyTo('reply@example.com') ->subject('Welcome!') ->html('<h1>Welcome</h1>') ->text('Welcome (plain text fallback)') ->transactional() ->withClickTracking(true) ->withOpenTracking(true) ->metadata(['user_id' => '123', 'campaign' => 'welcome']) ->substitutionData(['name' => 'John', 'company' => 'Acme']) ->tag('welcome') );
Quick Send Methods
// HTML email $response = Lettr::emails()->sendHtml( from: 'sender@example.com', to: 'recipient@example.com', subject: 'Hello', html: '<p>HTML content</p>', ); // Plain text email $response = Lettr::emails()->sendText( from: ['email' => 'sender@example.com', 'name' => 'Sender'], to: ['recipient1@example.com', 'recipient2@example.com'], subject: 'Hello', text: 'Plain text content', ); // Template email (subject is optional — the template defines its own subject) $response = Lettr::emails()->sendTemplate( from: 'sender@example.com', to: 'recipient@example.com', subject: null, templateSlug: 'welcome-email', templateVersion: 2, substitutionData: ['name' => 'John'], );
Attachments
use Lettr\Dto\Email\Attachment; $email = Lettr::emails()->create() ->from('sender@example.com') ->to(['recipient@example.com']) ->subject('Document attached') ->html('<p>Please find the document attached.</p>') // From file path ->attachFile('/path/to/document.pdf') // With custom name and mime type ->attachFile('/path/to/file', 'custom-name.pdf', 'application/pdf') // From binary data ->attachData($binaryContent, 'report.csv', 'text/csv') // Using Attachment DTO ->attach(Attachment::fromFile('/path/to/image.png')); $response = Lettr::emails()->send($email);
Templates with Substitution Data
When using a template, the subject is optional — the template defines its own subject. You can omit ->subject() entirely, or provide one to override the template's subject.
$response = Lettr::emails()->send( Lettr::emails()->create() ->from('sender@example.com') ->to(['recipient@example.com']) ->useTemplate('order-confirmation', version: 1) ->substitutionData([ 'order_id' => '12345', 'customer_name' => 'John Doe', 'items' => [ ['name' => 'Product A', 'price' => 29.99], ['name' => 'Product B', 'price' => 49.99], ], 'total' => 79.98, ]) );
Email Options
$email = Lettr::emails()->create() ->from('sender@example.com') ->to(['recipient@example.com']) ->subject('Newsletter') ->html($htmlContent) // Tracking ->withClickTracking(true) ->withOpenTracking(true) // Mark as non-transactional (marketing email, respects unsubscribe lists) ->transactional(false) // CSS inlining ->withInlineCss(true) // Template variable substitution ->withSubstitutions(true);
Retrieving Emails
Get Email Events by Request ID
use Lettr\Enums\EventType; // After sending $response = Lettr::emails()->send($email); $requestId = $response->requestId; // Later, retrieve events $result = Lettr::emails()->get($requestId); foreach ($result->events as $event) { echo $event->type->value; // 'delivery', 'open', 'click', etc. echo $event->recipient; // Recipient email echo $event->timestamp; // When the event occurred // Event-specific data if ($event->type === EventType::Click) { echo $event->clickUrl; } if ($event->type === EventType::Bounce) { echo $event->bounceClass; echo $event->reason; } }
List Email Events with Filtering
use Lettr\Dto\Email\ListEmailsFilter; // List all events $result = Lettr::emails()->list(); // With filters $filter = ListEmailsFilter::create() ->perPage(50) ->forRecipient('user@example.com') ->fromDate('2024-01-01') ->toDate('2024-12-31'); $result = Lettr::emails()->list($filter); echo $result->totalCount; echo $result->pagination->hasNextPage(); // Paginate through results while ($result->hasMore()) { foreach ($result->events as $event) { // Process event } $filter = $filter->cursor($result->pagination->nextCursor); $result = Lettr::emails()->list($filter); }
Domain Management
List Domains
$domains = Lettr::domains()->list(); foreach ($domains as $domain) { echo $domain->domain; // example.com echo $domain->status->value; // 'pending', 'approved' echo $domain->canSend; // true/false }
Add a Domain
use Lettr\ValueObjects\DomainName; $result = Lettr::domains()->create('example.com'); echo $result->domain; echo $result->status; // DNS records to configure echo $result->dns->returnPathHost; echo $result->dns->returnPathValue; if ($result->dns->dkim !== null) { echo $result->dns->dkim->selector; echo $result->dns->dkim->publicKey; }
Verify Domain DNS
$verification = Lettr::domains()->verify('example.com'); if ($verification->isFullyVerified()) { echo "Domain is ready to send!"; } else { if (!$verification->dkim->isValid()) { echo "DKIM error: " . $verification->dkim->error; } if (!$verification->returnPath->isValid()) { echo "Return path error: " . $verification->returnPath->error; } }
Get Domain Details
$domain = Lettr::domains()->get('example.com'); echo $domain->domain; echo $domain->status; echo $domain->trackingDomain; echo $domain->createdAt;
Delete a Domain
Lettr::domains()->delete('example.com');
Webhooks
List Webhooks
$webhooks = Lettr::webhooks()->list(); foreach ($webhooks as $webhook) { echo $webhook->id; echo $webhook->name; echo $webhook->url; echo $webhook->enabled; echo $webhook->authType->value; // 'none', 'basic', 'oauth2' foreach ($webhook->eventTypes as $eventType) { echo $eventType->value; } if ($webhook->isFailing()) { echo "Last error: " . $webhook->lastError; } }
Get Webhook Details
use Lettr\Enums\EventType; $webhook = Lettr::webhooks()->get('webhook-id'); echo $webhook->name; echo $webhook->url; echo $webhook->lastTriggeredAt; if ($webhook->listensTo(EventType::Bounce)) { echo "Webhook receives bounce notifications"; }
Event Types
The SDK provides an EventType enum with helper methods:
use Lettr\Enums\EventType; $type = EventType::Delivery; $type->label(); // "Delivery" $type->isSuccess(); // true (injection, delivery) $type->isFailure(); // false (bounce, policy_rejection, etc.) $type->isEngagement(); // false (open, initial_open, click) $type->isUnsubscribe(); // false (list_unsubscribe, link_unsubscribe)
Available event types: injection, delivery, bounce, delay, policy_rejection, out_of_band, open, initial_open, click, generation_failure, generation_rejection, spam_complaint, list_unsubscribe, link_unsubscribe
Error Handling
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()); }
Configuration
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.
CLI Commands
lettr:check
Verify that your Lettr integration is correctly configured:
php artisan lettr:check
Checks mailer registration, API key validity, and sending domain verification. Returns exit code 0 if all checks pass.
lettr:pull
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 |
lettr:generate-enum
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-run
Generates an enum like:
enum LettrTemplate: string { case WelcomeEmail = 'welcome-email'; case OrderConfirmation = 'order-confirmation'; }
lettr:generate-dtos
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-run
Generated 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);
Development
Install Dependencies
composer install
Code Style
composer lint
Static Analysis
composer analyse
Testing
composer test
Contributing
Please see CONTRIBUTING for details.
License
MIT License. See LICENSE for details.