Easily integrate Lettermint into your Laravel application.
- PHP 8.2 or higher
- Laravel 9 or higher
You can install the package via composer:
composer require lettermint/lettermint-laravelYou can publish the config file with:
php artisan vendor:publish --tag="lettermint-config"This creates a config/lettermint.php file where you can add your API token.
Add your Lettermint API token in your .env file:
LETTERMINT_TOKEN=your-lettermint-tokenOr update the config/lettermint.php file as needed.
In your config/mail.php, set the default option to lettermint:
'lettermint' => [
'transport' => 'lettermint',
],In your config/services.php, add the Lettermint service:
'lettermint' => [
'token' => env('LETTERMINT_TOKEN'),
],If you would like to specify the Lettermint route that should be used by a given mailer, you may add the route_id configuration option to the mailer's configuration array in your config/mail.php file:
'lettermint' => [
'transport' => 'lettermint',
'route_id' => env('LETTERMINT_ROUTE_ID'),
],You can configure multiple mailers using the same Lettermint transport but with different route IDs:
// config/mail.php
'mailers' => [
'lettermint_marketing' => [
'transport' => 'lettermint',
'route_id' => env('LETTERMINT_MARKETING_ROUTE_ID'),
],
'lettermint_transactional' => [
'transport' => 'lettermint',
'route_id' => env('LETTERMINT_TRANSACTIONAL_ROUTE_ID'),
],
],Then use them in your application:
Mail::mailer('lettermint_marketing')->to($user)->send(new MarketingEmail());
Mail::mailer('lettermint_transactional')->to($user)->send(new TransactionalEmail());The Lettermint Laravel driver prevents duplicate email sends by using idempotency keys. This is especially useful when emails are sent from queued jobs that might be retried.
You can configure idempotency behavior per mailer in your config/mail.php:
'mailers' => [
'lettermint' => [
'transport' => 'lettermint',
'idempotency' => true, // Enable automatic content-based idempotency
'idempotency_window' => 86400, // Window in seconds (default: 24 hours)
],
'lettermint_marketing' => [
'transport' => 'lettermint',
'route_id' => 'marketing',
'idempotency' => false, // Disable automatic idempotency
],
],idempotency: Enable/disable automatic content-based idempotencytrue: Generates idempotency keys based on email contentfalse(default): Disables automatic idempotency (user headers still work)
idempotency_window: Time window in seconds for deduplication- Default:
86400(24 hours to match Lettermint API retention) - Set to match your needs (e.g.,
3600for 1 hour,300for 5 minutes) - When set to
86400or higher, emails with identical content are permanently deduplicated within the API retention period
- Default:
When idempotency is true, the driver generates a unique key based on:
- Email subject, recipients (to, cc, bcc), and content
- Sender address (to differentiate between different sending contexts)
- Time window (if less than 24 hours)
This ensures:
- Identical emails are only sent once within the configured time window
- Retried queue jobs won't create duplicate emails
- Different emails or the same email after the time window will be sent normally
You can override any configuration by setting a custom idempotency key in the email headers:
Mail::send('emails.welcome', $data, function ($message) {
$message->to('user@example.com')
->subject('Welcome!')
->getHeaders()->addTextHeader('Idempotency-Key', 'welcome-user-123');
});Priority order (highest to lowest):
Idempotency-Keyheader in the email (always respected, overrides any config)- Automatic Message-ID (if
idempotencyistrue) - No idempotency (if
idempotencyisfalse)
Important: The idempotency: false configuration only disables automatic idempotency. User-provided Idempotency-Key headers are always respected, giving users full control on a per-email basis.
The Lettermint Laravel driver supports adding tags and metadata to your emails for better organization, tracking, and analytics.
Tags help you categorize and filter your emails in the Lettermint dashboard. You can add tags using Laravel's native mailable methods:
use App\Mail\WelcomeEmail;
use Illuminate\Support\Facades\Mail;
Mail::send((new WelcomeEmail($user))
->tag('onboarding')
);use Illuminate\Mail\Mailables\Envelope;
class WelcomeEmail extends Mailable
{
public function envelope(): Envelope
{
return new Envelope(
subject: 'Welcome to our platform!',
tags: ['onboarding'], // Only one tag is allowed
);
}
}To minimise confusion with the way of tagging emails sent via the SMTP relay, the Lettermint Laravel driver also supports the X-LM-Tag header.
This will be converted to the TagHeader envelope method automatically.
use Illuminate\Mail\Mailables\Headers;
class WelcomeEmail extends Mailable
{
public function headers(): Headers
{
return new Headers(
text: [
'X-LM-Tag' => 'onboarding',
],
);
}
}Metadata allows you to attach custom key-value pairs to your emails for enhanced tracking and analytics:
Mail::send((new OrderConfirmation($order))
->metadata('order_id', $order->id)
->metadata('customer_id', $order->customer_id)
);public function envelope(): Envelope
{
return new Envelope(
subject: 'Order Confirmation',
metadata: [
'order_id' => $this->order->id,
'customer_id' => $this->order->customer_id,
'order_total' => $this->order->total,
],
);
}You can use both tags and metadata together:
Mail::send((new OrderShipped($order))
->tag('transactional')
->metadata('order_id', $order->id)
->metadata('tracking_number', $order->tracking_number)
);Or in your mailable:
public function envelope(): Envelope
{
return new Envelope(
subject: 'Your order has shipped!',
tags: ['transactional', 'shipping'],
metadata: [
'order_id' => $this->order->id,
'tracking_number' => $this->order->tracking_number,
'carrier' => $this->order->carrier,
],
);
}- The driver supports Laravel's native
tag()andmetadata()methods (Laravel 9+) - The
X-LM-Tagheader is supported for backward compatibility - When both
TagHeaderandX-LM-Tagare present, theTagHeadertakes precedence
The package provides built-in support for handling Lettermint webhooks with automatic signature verification.
Add your webhook signing secret to your .env file:
LETTERMINT_WEBHOOK_SECRET=your-webhook-signing-secretYou can optionally configure the route prefix and timestamp tolerance:
LETTERMINT_WEBHOOK_PREFIX=lettermint
LETTERMINT_WEBHOOK_TOLERANCE=300Or publish the config file and modify the webhooks section:
// config/lettermint.php
'webhooks' => [
'secret' => env('LETTERMINT_WEBHOOK_SECRET'),
'prefix' => env('LETTERMINT_WEBHOOK_PREFIX', 'lettermint'),
'tolerance' => env('LETTERMINT_WEBHOOK_TOLERANCE', 300),
],The package automatically registers a webhook endpoint at:
POST /{prefix}/webhook
By default, this is POST /lettermint/webhook. Configure this URL in your Lettermint dashboard.
The package dispatches Laravel events for each webhook type. Listen to specific events in your EventServiceProvider or using closures:
use Lettermint\Laravel\Events\MessageDelivered;
use Lettermint\Laravel\Events\MessageHardBounced;
use Lettermint\Laravel\Events\MessageSpamComplaint;
// In EventServiceProvider
protected $listen = [
MessageDelivered::class => [
HandleEmailDelivered::class,
],
MessageHardBounced::class => [
HandleEmailBounced::class,
],
];
// Or using closures
Event::listen(MessageDelivered::class, function (MessageDelivered $event) {
Log::info('Email delivered', [
'message_id' => $event->data->messageId,
'recipient' => $event->data->recipient,
'status_code' => $event->data->response->statusCode,
]);
});
Event::listen(MessageHardBounced::class, function (MessageHardBounced $event) {
// Handle permanent bounce - consider disabling the recipient
$recipient = $event->data->recipient;
$reason = $event->data->response->content;
});| Event Class | Webhook Type | Description |
|---|---|---|
MessageCreated |
message.created |
Message accepted for processing |
MessageSent |
message.sent |
Message sent to recipient server |
MessageDelivered |
message.delivered |
Message successfully delivered |
MessageHardBounced |
message.hard_bounced |
Permanent delivery failure |
MessageSoftBounced |
message.soft_bounced |
Temporary delivery failure |
MessageSpamComplaint |
message.spam_complaint |
Recipient reported spam |
MessageFailed |
message.failed |
Processing failure |
MessageSuppressed |
message.suppressed |
Message suppressed |
MessageUnsubscribed |
message.unsubscribed |
Recipient unsubscribed |
MessageInbound |
message.inbound |
Inbound email received |
WebhookTest |
webhook.test |
Test event from dashboard |
You can listen to all webhook events using the base class:
use Lettermint\Laravel\Events\LettermintWebhookEvent;
Event::listen(LettermintWebhookEvent::class, function (LettermintWebhookEvent $event) {
Log::info('Webhook received', [
'type' => $event->getEnvelope()->event->value,
'id' => $event->getEnvelope()->id,
]);
});Each event has two main properties:
$event->envelope- Common webhook envelope (id, event type, timestamp)$event->data- Event-specific typed payload
// Envelope (common to all events)
$event->envelope->id; // Webhook event ID (string)
$event->envelope->event; // WebhookEventType enum
$event->envelope->timestamp; // DateTimeImmutable
// Data (typed per event)
// For MessageDelivered:
$event->data->messageId; // string
$event->data->recipient; // string
$event->data->response->statusCode; // int
$event->data->response->content; // string|null
$event->data->metadata; // array
$event->data->tag; // string|nullEach event type has its own typed data class:
| Event | Data Properties |
|---|---|
MessageDelivered |
messageId, recipient, response, metadata, tag |
MessageHardBounced |
messageId, recipient, response, metadata, tag |
MessageCreated |
messageId, from, to, cc, bcc, subject, metadata, tag |
MessageInbound |
route, messageId, from, to, subject, body, headers, attachments, isSpam, spamScore |
WebhookTest |
message, webhookId, timestamp |
The WebhookEventType enum provides helper methods:
$event->envelope->event->isBounce(); // true for hard/soft bounces
$event->envelope->event->isDeliveryIssue(); // true for bounces, failed, suppressedcomposer testPlease see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
The MIT License (MIT). Please see License File for more information.