letmesendemail / letmesendemail-laravel
letmesend.email for Laravel.
Package info
github.com/letmesendemail/letmesendemail-laravel
pkg:composer/letmesendemail/letmesendemail-laravel
Requires
- php: ^8.1
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
- letmesendemail/letmesendemail-php: ^0.2
- symfony/mailer: ^6.2|^7.0|^8.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.13
- mockery/mockery: ^1.6
- orchestra/testbench: ^8.17|^9.0|^10.8|^11.0
- pestphp/pest: ^1.0|^2.0|^3.0|^4.0
- phpstan/phpstan: ^1.10
This package is auto-updated.
Last update: 2026-07-14 12:39:49 UTC
README
The official Laravel package for the letmesend.email API.
Full Documentation
See the comprehensive user manual for complete documentation of every resource, configuration option, mail transport, webhooks, error handling, and detailed examples.
Requirements
- PHP 8.1+
- Laravel 10, 11, 12, or 13
Installation
composer require letmesendemail/letmesendemail-laravel
Configuration
Set your API key in .env:
LETMESENDEMAIL_API_KEY=lms_live_...
Optionally configure the base URL, timeout, and retries:
LETMESENDEMAIL_BASE_URL=https://letmesend.email/api/v1 LETMESENDEMAIL_TIMEOUT=30 LETMESENDEMAIL_RETRIES=3
Publish the config file (optional):
php artisan vendor:publish --tag=letmesendemail-config
Environment Variables
| Variable | Default | Description |
|---|---|---|
LETMESENDEMAIL_API_KEY |
— | Your letmesend.email API key |
LETMESENDEMAIL_BASE_URL |
https://letmesend.email/api/v1 |
API base URL |
LETMESENDEMAIL_TIMEOUT |
30 |
Request timeout in seconds |
LETMESENDEMAIL_RETRIES |
0 |
Retry attempts for transient failures |
LETMESENDEMAIL_WEBHOOK_SECRET |
— | Webhook signing secret |
LETMESENDEMAIL_WEBHOOKS_ENABLED |
false |
Enable webhook route |
LETMESENDEMAIL_WEBHOOK_PATH |
/webhooks/letmesendemail |
Webhook URI path |
Explicit Configuration
For tests and multi-tenant applications, configure the client explicitly:
use LetMeSendEmail\Laravel\LetMeSendEmail; $client = new LetMeSendEmail( apiKey: 'lms_live_...', baseUrl: 'https://letmesend.email/api/v1', timeout: 60, retries: 5, );
You may also inject a preconfigured core Client or a TransportInterface for testing:
use LetMeSendEmail\Client; use LetMeSendEmail\Configuration; use LetMeSendEmail\Http\GuzzleTransport; use GuzzleHttp\Client as GuzzleClient; $httpClient = new LetMeSendEmail( client: new Client( new Configuration(apiKey: '...', retries: 5), new GuzzleTransport(new GuzzleClient()), ), );
Usage
Facade
use LetMeSendEmail\Laravel\Facades\LetMeSendEmail;
Emails
// Send $email = LetMeSendEmail::emails()->send( from: 'Acme <hello@acme.com>', to: ['person@example.com'], subject: 'Welcome', html: '<p>Hello from letmesend.email</p>', ); echo $email->getId(); // Send with template $email = LetMeSendEmail::emails()->sendWithTemplate( from: 'Acme <hello@acme.com>', to: ['person@example.com'], templateId: '01ARZ3NDEKTSV4RRFFQ69G5FAV', templateVariables: [ ['key' => 'USER_NAME', 'type' => 'string', 'value' => 'John'], ], ); // Verify email $result = LetMeSendEmail::emails()->verify('person@example.com'); echo $result->getStatus(); // List emails (cursor-based pagination) $list = LetMeSendEmail::emails()->list(perPage: 20); foreach ($list->items() as $email) { echo $email->getId() . ' - ' . $email->getSubject(); } echo $list->pagination()->hasMore(); // true // Next page $list = LetMeSendEmail::emails()->list(perPage: 20, after: 'cursor_from_previous_page'); // Get email $email = LetMeSendEmail::emails()->get('01kvv5dv472evp42a60sy4p7zx');
Domains
$list = LetMeSendEmail::domains()->list(); $domain = LetMeSendEmail::domains()->get($id); $result = LetMeSendEmail::domains()->verify('example.com');
Contacts
$contact = LetMeSendEmail::contacts()->create( email: 'john@example.com', firstName: 'John', lastName: 'Doe', ); $list = LetMeSendEmail::contacts()->list(); $contact = LetMeSendEmail::contacts()->get($id); $updated = LetMeSendEmail::contacts()->update($id, firstName: 'Jane'); $result = LetMeSendEmail::contacts()->delete($id);
Contact Categories
$category = LetMeSendEmail::contactCategories()->create(name: 'New Name'); $list = LetMeSendEmail::contactCategories()->list(); $category = LetMeSendEmail::contactCategories()->get($id); $category = LetMeSendEmail::contactCategories()->update($id, name: 'Updated'); $result = LetMeSendEmail::contactCategories()->delete($id);
Email Topics
$topic = LetMeSendEmail::emailTopics()->create( name: 'Product Updates', slug: 'product-updates', ); $list = LetMeSendEmail::emailTopics()->list(); $topic = LetMeSendEmail::emailTopics()->get($id); $topic = LetMeSendEmail::emailTopics()->update($id, name: 'Updated'); $result = LetMeSendEmail::emailTopics()->delete($id);
Laravel Mail Transport
Send emails through Laravel's mail system using the letmesendemail mailer.
Configuration
Set your .env mailer:
MAIL_MAILER=letmesendemail
Or configure config/mail.php:
'mailers' => [ 'letmesendemail' => [ 'transport' => 'letmesendemail', ], ],
Sending a Mailable
namespace App\Mail; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Queue\SerializesModels; class WelcomeEmail extends Mailable { use Queueable, SerializesModels; public function build(): static { return $this ->from('noreply@acme.com') ->subject('Welcome!') ->html('<h1>Welcome</h1>'); } }
Mail::to('user@example.com')->send(new WelcomeEmail());
Attachments
use Illuminate\Mail\Mailables\Attachment; $this->attachFromStorage('/path/to/report.pdf'); // or $this->attachData('file content', 'report.txt', ['mime' => 'text/plain']);
Structural MIME headers (From, To, Cc, Bcc, Reply-To, Subject, Content-Type, MIME-Version, Date, Message-ID, Sender, Return-Path) are automatically excluded from the API custom headers.
Idempotency
Set an Idempotency-Key header on the Mailable:
use Illuminate\Support\Facades\Mail; use Symfony\Component\Mime\Email; Mail::to('user@example.com')->send( (new WelcomeEmail()) ->withSymfonyMessage(function (Email $message) { $message->getHeaders()->addTextHeader('Idempotency-Key', 'my-unique-key'); }), );
The SDK detects Idempotency-Key case-insensitively and passes it through the core API's
idempotencyKey parameter.
Queue
Mail::to('user@example.com')->queue(new WelcomeEmail());
The transport's ApiException mapping to Symfony TransportException works in queued jobs.
Testing
use Illuminate\Support\Facades\Mail; Mail::fake(); Mail::assertSent(WelcomeEmail::class);
Pagination
List endpoints return a response with cursor-based pagination:
$list = LetMeSendEmail::emails()->list(perPage: 10); foreach ($list->items() as $email) { echo $email->getId(); } $pag = $list->pagination(); $pag->hasMore(); // bool $pag->getTotal(); // int $pag->getPerPage(); // int // Next page $next = LetMeSendEmail::emails()->list(perPage: 10, after: 'cursor_value'); // Previous page $prev = LetMeSendEmail::emails()->list(perPage: 10, before: 'cursor_value');
Error Handling
use LetMeSendEmail\Exceptions\ValidationError; use LetMeSendEmail\Exceptions\AuthenticationError; use LetMeSendEmail\Exceptions\RateLimitError; use LetMeSendEmail\Exceptions\ApiException; try { LetMeSendEmail::emails()->send(/* ... */); } catch (ValidationError $e) { // field-level errors: $e->getValidationErrors() } catch (AuthenticationError $e) { // check API key } catch (RateLimitError $e) { // retry after $e->getRetryAfter() } catch (ApiException $e) { // HTTP status: $e->getHttpStatus() // API code: $e->getApiCode() }
| Exception | HTTP Status | Description |
|---|---|---|
ValidationError |
400, 413, 422 | Request validation failed |
AuthenticationError |
401 | Invalid or missing API key |
AuthorizationError |
403 | Insufficient permissions |
NotFoundError |
404 | Resource not found |
ConflictError |
409 | Resource conflict |
RateLimitError |
429 | Rate limit exceeded |
ApiError |
500+ | Server error |
NetworkError |
— | Connection failed |
TimeoutError |
— | Request timed out |
Webhooks
Configuration
Enable webhooks in your .env:
LETMESENDEMAIL_WEBHOOKS_ENABLED=true LETMESENDEMAIL_WEBHOOK_SECRET=whsec_your_signing_secret
The webhook route is registered at /webhooks/letmesendemail by default. It uses
the VerifyWebhookSignature middleware (aliased as letmesendemail.webhook) which
verifies the signature before the controller executes.
How it works
- The middleware reads the raw request body and webhook headers, calls
WebhookSignature::verify(), and stores the parsed payload on the request. - If the signature is invalid, the middleware returns a 400 response.
- The controller reads the verified payload from the request and dispatches
LetMeSendEmail\Laravel\Events\WebhookReceived.
Listening for webhooks
namespace App\Listeners; use LetMeSendEmail\Laravel\Events\WebhookReceived; class HandleLetMeSendEmailWebhook { public function handle(WebhookReceived $event): void { match ($event->payload['event'] ?? '') { 'email.delivered' => // handle delivery 'email.bounced' => // handle bounce default => // unknown event }; } }
Register the listener in EventServiceProvider:
protected $listen = [ \LetMeSendEmail\Laravel\Events\WebhookReceived::class => [ \App\Listeners\HandleLetMeSendEmailWebhook::class, ], ];
Timestamp tolerance
The default tolerance is 300 seconds (5 minutes). Configure via config:
// config/letmesendemail.php 'webhooks' => [ 'tolerance' => 300, ],
Testing
composer install vendor/bin/pest
Mail::fake with the letmesendemail transport
use Illuminate\Support\Facades\Mail; Mail::fake(); Mail::assertSent(WelcomeEmail::class);
Changelog
See CHANGELOG.md.