lettr/lettr-laravel

Lettr for Laravel - Official Laravel integration for Lettr email API

Installs: 22

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/lettr/lettr-laravel

v0.10.0 2026-02-04 16:34 UTC

This package is auto-updated.

Last update: 2026-02-04 16:35:31 UTC


README

CI Latest Version on Packagist Total Downloads PHP Version License

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_ADDRESS to 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', $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 envelope(): Envelope
    {
        return new Envelope(
            from: 'hello@example.com',
            subject: 'Welcome to Our App!',
        );
    }

    public function build(): static
    {
        return $this
            ->template('welcome-email', version: 2, projectId: 123)
            ->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, $projectId) Set template slug with optional version and project
templateVersion($version) Set template version separately
projectId($projectId) Set project ID 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')
            ->projectId(config('services.lettr.project_id'))
            ->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:

use Illuminate\Support\Facades\Mail;

// Simple usage
Mail::lettr()
    ->to('user@example.com')
    ->sendTemplate('welcome-email', ['name' => 'John']);

// With version and project ID
Mail::lettr()
    ->to('user@example.com')
    ->sendTemplate('order-confirmation', [
        'order_id' => 123,
        'items' => $items,
    ], version: 2, projectId: 456);

// With CC and BCC
Mail::lettr()
    ->to('user@example.com')
    ->cc('manager@example.com')
    ->bcc('records@example.com')
    ->sendTemplate('invoice', $invoiceData);

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', ['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', ['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'])
        ->campaignId('welcome-series')
);

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
$response = Lettr::emails()->sendTemplate(
    from: 'sender@example.com',
    to: 'recipient@example.com',
    subject: 'Welcome!',
    templateSlug: 'welcome-email',
    templateVersion: 2,
    projectId: 123,
    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

$response = Lettr::emails()->send(
    Lettr::emails()->create()
        ->from('sender@example.com')
        ->to(['recipient@example.com'])
        ->subject('Your Order #{{order_id}}')
        ->useTemplate('order-confirmation', version: 1, projectId: 123)
        ->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 transactional (bypasses 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', 'bearer'

    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;

try {
    $response = Lettr::emails()->send($email);
} 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'),
];

The package also supports config('services.lettr.key') as a fallback for the API key.

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.