Laravel
Use the official Lettermint Laravel package 3.0 with Mailables, notifications, queued mail, tags, metadata, and idempotency. It also registers one shared Lettermint client for direct API calls, and receives verified webhooks as Laravel events. The package uses the PHP SDK 3.0. See Integrations for other packages.
Upgrading from 2.x? Follow the Laravel package 3.0 upgrade guide on GitHub.
Requirements
Before you start you need:
- PHP 8.2 or newer
- Laravel 10, 11, 12, or 13
- A Lettermint account with a verified sending domain
- A Project API token from your project settings
- Optional: a Team API token, to manage domains, projects, routes, and messages from your app
If your domain is not verified yet, follow the domain setup guide first so your mail reaches the inbox instead of the spam folder.
1. Installation
Install the package via Composer:
Code
Publish the configuration file:
Code
2. Configuration
Environment variables
Add your Lettermint credentials to .env:
.env
| Variable | Config key | Used by |
|---|---|---|
LETTERMINT_PROJECT_TOKEN | lettermint.token | The mail transport and Lettermint::emails() |
LETTERMINT_TEAM_TOKEN | lettermint.api_token | Every other part of the client |
Configure one token or both. Each part uses its own token and never falls back to the other one. With only a project token you can send email, and a Team API call throws a LettermintConfigException that names the missing token before any request is made.
The names used by earlier versions still work. LETTERMINT_TOKEN is read when LETTERMINT_PROJECT_TOKEN is not set, and LETTERMINT_API_TOKEN when LETTERMINT_TEAM_TOKEN is not set. Prefer the new names in new applications.
Requests time out after 15 seconds, covering the whole request including reading the response. Change it with LETTERMINT_TIMEOUT=30 (seconds).
Services configuration
Add to config/services.php:
config/services.php
api_token is only needed for the Team API. Use a Team API token for that value. Values in config/lettermint.php take precedence over config/services.php.
Mail configuration
Add the Lettermint mailer to config/mail.php:
config/mail.php
Set Lettermint as your default mailer in .env:
.env
3. Send your first email
Send emails using Laravel's standard Mail facade:
Code
Or specify the mailer explicitly:
Code
Queued and bulk sending
Because Lettermint is a standard Laravel mail driver, it works with Laravel's queue and notification systems out of the box. Queue a message so sending happens in the background:
Code
Or have a Mailable always queue by implementing ShouldQueue:
Code
For higher volumes, dispatch onto a dedicated queue and process it with Horizon or queue:work, exactly as you would with any Laravel mailer.
Handle errors
When Lettermint rejects an email or cannot be reached, the transport throws Symfony's TransportException. Its code is the HTTP status (0 when no response was received), and getPrevious() returns the SDK exception with the details:
Code
See the PHP SDK guide for every exception class.
4. Tags
Categorize emails for filtering and analytics in your Lettermint dashboard.
Using the tag() method
Code
In mailable envelope
Code
This SDK example uses the existing singular tag. It has no deprecation date. Use the raw HTTP API to send structured tags until this SDK has a multi-tag method. See Tags documentation.
5. Metadata
Attach custom data for tracking and webhook payloads.
Using the metadata() method
Code
In mailable envelope
Code
Metadata is included in webhook payloads but not sent to recipients.
6. Multiple routes
Configure separate mailers for different email types (transactional, marketing, etc.):
config/mail.php
.env
Send to specific routes:
Code
7. Idempotency
Prevent duplicate emails when retrying failed requests.
Configuration
config/mail.php
Custom idempotency key
Override the automatic key for specific emails. The Idempotency-Key header is sent as a request header, not as an email header, and always takes precedence over the automatic key:
Code
The automatic key is a hash of the subject, the recipients, the body, and the sender. Keep idempotency_window at 86400 seconds to align with Lettermint's 24-hour idempotency key validity. Keys are scoped per project; retrying the same payload returns the cached 202 Accepted response, while reusing a key with a different payload returns 409 Conflict.
8. Use the Lettermint client
The package registers one Lettermint\Lettermint client in the container, built from your configuration. It holds no message state, so the same instance serves the mail transport, queue workers, and Octane. Use it through the Lettermint facade, dependency injection, or the container.
Send an email with the facade. The idempotency key is an argument of the call:
Code
Or inject the client. On an injected client, the parts are properties ($lettermint->emails, $lettermint->domains), and compose() returns an immutable builder:
Code
The facade exposes each part of the client as a method (Lettermint::emails(), Lettermint::domains(), and so on) and forwards the client's methods, such as Lettermint::ping(). The PHP guide covers the email builder, batch sending, scheduling, pagination, and response types.
Team API
With LETTERMINT_TEAM_TOKEN configured, the same facade reaches the Team API:
Code
Queued jobs
The client refuses to be serialized because it holds your tokens. Do not store it in a property of a queued job, event, or listener. Resolve it where you use it, for example as a handle() argument:
Code
Custom HTTP client
To send the client's requests through your own Guzzle client, for example with a proxy or a test handler, bind it before the client is first resolved:
Code
9. Webhooks
The package receives Lettermint webhooks, verifies their signature with the PHP SDK, and dispatches a Laravel event for each delivery.
Configuration
Add the webhook's signing secret, including its whsec_ prefix, to .env:
.env
The prefix and the timestamp tolerance (in seconds) are optional. The package registers the endpoint POST /{prefix}/webhook, named lettermint.webhook. By default that is POST /lettermint/webhook. Add its URL to the webhook in your Lettermint dashboard.
Verification
Every request must carry the X-Lettermint-Signature and X-Lettermint-Delivery headers that Lettermint sends, and the raw body exactly as it was sent. If a proxy or load balancer sits in front of your application, make sure it forwards both headers and does not change the body.
A request that fails verification gets a 401 response whose reason says why, for example signature_mismatch or timestamp_out_of_tolerance, and no event is dispatched. A verified delivery is acknowledged with 200.
Custom webhook routes
To register the endpoint yourself, for example on a specific domain, disable the automatic route:
.env
Then register the controller and the signature middleware in your routes file:
Code
You can wrap the registration in Route::domain(...) or another route group. If you handle the request in your own controller, VerifyWebhookSignature::payload($request) returns the verified Lettermint\WebhookPayload. The package's own route runs without the web middleware group. If you register the route in routes/web.php, exclude it from CSRF validation so Laravel does not reject Lettermint's request.
Available events
| Event class | Webhook type | Description |
|---|---|---|
MessageCreated | message.created | Message accepted for processing |
MessageSent | message.sent | Message sent to the recipient's server |
MessageDelivered | message.delivered | Message 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 |
MessagePolicyRejected | message.policy_rejected | Rejected by the sending policy |
MessageUnsubscribed | message.unsubscribed | Recipient unsubscribed |
MessageOpened | message.opened | Recipient opened the email |
MessageClicked | message.clicked | Recipient clicked a link |
MessageInbound | message.inbound | Inbound email received |
MessageAutoReplied | message.auto_replied | An automatic reply was received |
MessageScheduled | message.scheduled | A message was scheduled |
MessageRescheduled | message.rescheduled | A message's schedule changed |
MessageCanceled | message.canceled | A scheduled message was canceled |
MessageReleased | message.released | A scheduled message was released |
SuppressionAdded | suppression.added | Suppression entry added |
SuppressionRemoved | suppression.removed | Suppression entry removed |
WebhookTest | webhook.test | Test event from the dashboard |
All event classes are in the Lettermint\Laravel\Events namespace.
Listening to events
Listen to the events you need:
app/Providers/AppServiceProvider.php
Or use listener classes:
app/Listeners/HandleEmailDelivered.php
Laravel 11 and newer discover a listener in app/Listeners from the type hint of its handle() method, so you register nothing. On Laravel 10, register it in EventServiceProvider:
app/Providers/EventServiceProvider.php
Do not register a discovered listener again by hand, or it runs twice.
Event structure
Each typed event has three properties:
Code
The typed data tolerates changes to the payload. A field Lettermint omits, or sends with an unexpected type, reads as null (or an empty list) instead of failing the delivery, and fields the package does not know stay available in $event->payload. Fields that Lettermint may omit or send as null are nullable, so check them before you use them. Identifiers that every delivery carries, such as messageId, are strings.
Inbound attachments are delivered as base64 content, or as a signed url (with expiresAt) when the route delivers attachment URLs. $attachment->getDecodedContent() returns the raw bytes, or null for a URL attachment.
The WebhookEventType enum groups event types:
Code
Unknown event types
When Lettermint sends an event type that your version of the package does not know yet, the delivery is still acknowledged with a 200 response, and an UnknownWebhookEventReceived event is dispatched with the event name and the complete verified payload. Update the package to receive a typed event instead.
Code
Listening to all events
Every webhook event implements the Lettermint\Laravel\Contracts\WebhookEvent interface, so one listener on it receives all of them: every typed event, and UnknownWebhookEventReceived.
Code
A listener on the abstract LettermintWebhookEvent class receives nothing. Laravel matches listeners on an event's own class and its interfaces, not its parent classes. Use the WebhookEvent interface.
FAQ
Which Laravel and PHP versions are supported?
PHP 8.2 or newer and Laravel 10, 11, 12, or 13. The package is tested against every supported Laravel version on PHP 8.2 to 8.5.
Do Mailables, notifications, and queued mail work as usual?
Yes. Lettermint registers as a standard mail transport, so everything you already build with the Mail facade, Mailables, notifications, and queues keeps working. Only the delivery happens through Lettermint.
How do I prevent duplicate emails?
Set 'idempotency' => true on the mailer, with a 24-hour window (idempotency_window). Retrying the same payload then returns the cached result instead of sending again. Override the key per message with an Idempotency-Key header, as shown in the idempotency section above.
How do I track deliveries and bounces?
The package verifies Lettermint's webhooks and dispatches Laravel events such as MessageDelivered and MessageHardBounced, so you can react in your app. For opens and clicks, enable tracking on the route your mailer uses.
Is Lettermint email GDPR compliant and EU-hosted?
Yes. Lettermint runs exclusively on European infrastructure and processes email in line with GDPR, so transactional mail from your Laravel app is handled inside the EU.
Next steps
Tags
Organize and filter emails with tags.
Tracking
Track opens, clicks, and deliverability.
Webhooks
Receive delivery events through webhooks.
SMTP Alternative
Send via SMTP instead of the API.
GitHub Repository
Find the complete source code, report issues, or contribute on GitHub.