somehow-digital / craft-lettermint
Lettermint Mailer Adapter for Craft CMS.
Package info
github.com/somehow-digital/craft-lettermint
Type:craft-plugin
pkg:composer/somehow-digital/craft-lettermint
Requires
- php: ^8.2
- craftcms/cms: ^5.8.0
- lettermint/symfony-mailer: ^1.1.0
Requires (Dev)
- craftcms/phpstan: dev-main
- pestphp/pest: ^3.0 || ^4.0
- phpstan/phpstan: ^2.1
- roave/security-advisories: dev-latest
- symplify/easy-coding-standard: ^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-08 21:27:37 UTC
README
Lettermint for Craft CMS
Lettermint integration for Craft CMS.
Features Β· Installation Β· Setup Β· Pro Edition Β· Support
Features
| Feature | Standard | Pro |
|---|---|---|
| π¬ Mail transport with route and tags | β | β |
| π Route and tags per site | β | |
| π·οΈ Craft metadata on every email | β | |
| π Utility with usage, statistics and checks | β | |
| π§© Dashboard widget | β | |
| π Webhooks as Craft events | β | |
| π©Ί Health endpoint for uptime monitors | β | |
| π₯οΈ Health command for deployments | β |
Requirements
- Craft CMS 5.8.0 or later
- PHP 8.2 or later
- A Lettermint account with a verified domain
Installation
Install the plugin from the Plugin Store in the Control Panel, or with Composer:
composer require somehow-digital/craft-lettermint php craft plugin/install lettermint
Setup
Three steps and your emails go out through Lettermint:
- Go to Settings β Email.
- Set Transport Type to Lettermint.
- Fill in the settings below and click Save.
| Setting | Required | Description |
|---|---|---|
| Project API Token | β | Found on your projects page under βAPI Tokenβ. |
| Route Slug | A custom route. Leave blank for the default transactional route. | |
| Tags | Tags for all emails, e.g. environment=production, campaign=welcome. Use only letters, digits, _ and -. |
Important
The System Email Address must use a domain you set up in Lettermint.
Tip
All settings support environment variables.
Pro Edition
Pro adds per-site settings, insights in the Control Panel, webhooks and health checks. Its settings appear below the standard settings β all of them are optional, so turn on only what you need.
| Setting | What it does |
|---|---|
| Team API Token | Unlocks the Team API features. Team owners create it in the dashboard under Manage team β βAPI Tokensβ. |
| Project | ID of the project your Project API Token belongs to. Needed for project details, statistics, domains and checks. |
| Webhook Secret | Turns on the webhook endpoint. |
| Health Token | Turns on the health endpoint. |
| Sites | Sets a route and tags per site. |
Note
Lettermint can't tell which project a token belongs to, so set Project yourself.
π Per-site routes and tags β send each site through its own route, with its own tags
Β
Set a Route Slug and Tags for each site in the Sites table. Both support environment variables.
The site comes from the message (e.g. a user's affiliated site) or the current request.
- Route Slug β the site's route replaces the adapter's route.
- Tags β the site's tags are merged with the adapter's tags. Tags with the same name are replaced.
Example: adapter environment=production, site=default + site site=shop β environment=production, site=shop
| Route Slug | Tags | |
|---|---|---|
| Site value set | Replaces the adapter's route | Merged with the adapter's tags |
| Site value empty | Adapter's route is used | Adapter's tags are used |
X-LM-Options header on the message |
Takes precedence | Replaces site and adapter tags |
| Limit | β | Max. 20 tags in total |
π·οΈ Craft metadata β see which site, user and system message an email belongs to
Β
Every email is sent with Craft context as Lettermint metadata. You can see it with each message in Lettermint.
| Key | Value |
|---|---|
craft_email |
Key of the system message, e.g. forgot_password |
craft_site |
Handle of the site |
craft_user |
ID of the recipient user, e.g. with setTo($user) |
craft_language |
Language of the email, e.g. de-AT |
craft_environment |
Craft environment, e.g. production |
Empty keys are skipped. For example, craft_email is missing for emails that aren't system messages.
Add your own metadata β metadata set on the message wins over the Craft metadata:
use Symfony\Component\Mailer\Header\MetadataHeader; $message->getSymfonyEmail()->getHeaders()->add(new MetadataHeader('order_id', '1234'));
π Utility β your team, usage, statistics and domains at a glance
Β
Go to Utilities β Lettermint for an overview of your team and project.
| Section | Shows |
|---|---|
| Team | Name, plan and verification |
| Usage | Emails sent this billing period vs. your plan |
| Tokens | Whether Lettermint accepts your API tokens |
| Project | Creation date, sandbox mode, SMTP, hidden email content, default route with open and click tracking (as far as Lettermint tells), token last used, and a link to the project in Lettermint |
| Health | Overall status and checks, with a link to the health endpoint for admins |
| Emails | Sent, delivered, hard bounces, spam complaints, opens and clicks over 28 days, with a chart |
| Domains | Verification status of the project's domains |
| Routes | Slug, type and default route of the project, with open and click tracking (as far as Lettermint tells) |
| Settings | One row for all sites and one per site with the System Email Address, sender name, reply-to address, HTML email template, route and tags, and whether webhooks and the health endpoint are on β never tokens or secrets |
βοΈ Project checks make sure that:
- the project is live,
- the System Email Address domain β also of each site that overrides it β belongs to the project and is verified,
- all route slugs (adapter and sites) are outgoing routes of the project.
π‘ Good to know
- Only the configured project is loaded, never other projects of the team.
- The Project API Token status works without a Team API Token.
- Sections the token can't read are hidden and show a notice.
- The page never waits for Lettermint: it loads its data in the background. Data is fresh for 5 minutes and shared with the dashboard widget. Older data is shown right away while it's reloaded, and before anything is loaded, the page shows its layout as a placeholder. Data is only cached when Lettermint could be reached, so an outage doesn't stick around.
- Click Refresh to reload the data. Saving other settings reloads it as well.
- Access requires the Utilities β Lettermint permission.
π§© Dashboard widget β a compact overview right on your dashboard
Β
Add the Lettermint widget to your dashboard. It's a compact version of the utility and shows:
- team and plan,
- usage this billing period,
- sent, delivered, hard bounces and spam complaints over 28 days, with a daily chart.
It shares its data with the utility. Only users with the Utilities β Lettermint permission can add or see it.
π Webhooks β react to deliveries, bounces and complaints with Craft events
Β
Receive Lettermint webhook events (deliveries, bounces, complaints, β¦) as Craft events.
Setup
- In the Lettermint dashboard, create a webhook and choose its events. Use the endpoint URL shown below the Webhook Secret setting, e.g.
https://example.com/actions/lettermint/webhook. - Copy its signing secret (
whsec_β¦) into Webhook Secret.
Usage
use SomehowDigital\Craft\Lettermint\services\WebhooksService; use SomehowDigital\Craft\Lettermint\events\WebhookEvent; use yii\base\Event; Event::on(WebhooksService::class, WebhooksService::EVENT_RECEIVE_WEBHOOK, function (WebhookEvent $event) { if ($event->type === WebhooksService::TYPE_MESSAGE_HARD_BOUNCED) { $event->payload->id; // delivery ID $event->payload->data; // event data, e.g. `recipient` } });
Every event type has a TYPE_* constant, e.g. TYPE_MESSAGE_DELIVERED or TYPE_SUPPRESSION_ADDED. Test deliveries from the dashboard use TYPE_WEBHOOK_TEST. New types from Lettermint fire the event too.
[!TIP]
- The same delivery can arrive more than once. Use
$event->payload->idto skip duplicates β it stays the same across retries.- Keep handlers fast. Push slow work to the queue.
$event->payloadis aSomehowDigital\Craft\Lettermint\models\WebhookPayloadwith theid,event,timestampanddataof the delivery.
Responses and retries
| Situation | Response | Lettermint retries? |
|---|---|---|
| Valid signature, handlers succeed | 200 |
β |
| Invalid signature or older than 5 minutes | 401 |
No |
| A handler throws an exception | 500 |
Yes, for up to ~14 hours |
| No response within 30 seconds | β | Yes |
Webhooks are also received while the system is offline.
π©Ί Health endpoint β let your uptime monitor know if emails can be sent
Β
Check if emails can be sent, e.g. from an uptime monitor.
Setup
- Generate a token with at least 16 characters and no spaces, e.g.
openssl rand -hex 32. - Store it in an environment variable, e.g.
LETTERMINT_HEALTH_TOKEN. - Set Health Token to
$LETTERMINT_HEALTH_TOKEN.
[!WARNING] Don't enter the token directly. It would be saved in the project config and end up in version control.
Usage
curl -H "Authorization: Bearer $LETTERMINT_HEALTH_TOKEN" https://example.com/actions/lettermint/health
The overall status is the worst check result: ok, warning or error.
| Status | Response |
|---|---|
ok or warning |
200 |
error |
503 |
| Missing/wrong token | 401 |
| Endpoint turned off | 404 |
The endpoint is off without a Health Token or in the Standard edition. Results are cached for 60 seconds to stay within Lettermint's rate limits. The endpoint also works while the system is offline.
Example response
{
"status": "warning",
"date": "2026-11-05T10:00:00+00:00",
"checks": [
{ "id": "token", "status": "ok", "text": "Lettermint accepts the project API token." },
{ "id": "teamToken", "status": "ok", "text": "Lettermint accepts the team API token." },
{ "id": "usage", "status": "warning", "text": "The monthly volume of the plan is nearly used up, with 46000 of 50000 emails." },
{ "id": "delivery", "status": "ok", "text": "The project is live." },
{ "id": "sender", "status": "ok", "text": "The domain βexample.comβ of the system email address belongs to the project." },
{ "id": "route", "status": "ok", "text": "Emails use the default route βoutgoingβ of the project." }
],
"usage": { "used": 46000, "included": 50000, "share": 0.92 }
}
date shows when the checks ran. Match on the check id, not the text β the text is translated.
All checks
| Check | Needs | Fails with |
|---|---|---|
token |
β | error: token rejected or Lettermint unreachable |
teamToken |
Team API Token | warning: token rejected or team unreadable |
usage |
Team API Token | warning: 90% or more of the monthly volume used |
project |
Team API Token | warning: project not in the team or couldn't be loaded |
delivery |
Team API Token + Project | warning: project in sandbox mode |
sender, sender.<site> |
Team API Token + Project | error: domain not in the project, warning: not verified |
route, route.<site> |
Team API Token + Project | error: not an outgoing route of the project |
Can't send headers, or getting 401?
- If your monitor can't send headers, use
?key=β¦instead. Prefer the header when possible β query strings end up in server logs. - Some Apache setups drop the
Authorizationheader before it reaches PHP. If you get401with the right token, pass the header through or use?key=β¦.
π₯οΈ Health command β run the health checks during a deployment
Β
Run the same checks as the health endpoint from the command line. No Health Token needed, and results are never cached.
php craft lettermint/health
ok token Lettermint accepts the project API token.
ok teamToken Lettermint accepts the team API token.
warning usage The monthly volume of the plan is nearly used up, with 46000 of 50000 emails.
Status: warning
| Option | Description |
|---|---|
--json |
Print the report as JSON, like the endpoint |
--strict |
Fail on warning too |
| Exit code | When |
|---|---|
0 |
ok or warning |
69 |
error, or warning with --strict |
78 |
Lettermint isn't the transport, or Standard edition |
Support
- π Found a bug or have an idea? Open an issue.
- βοΈ Need help? Write to support@somehow.digital.
- π See what's new in the changelog.
License
Released under the MIT License.