Conversation
Replace the builder mocks with a real EmailEndpoint backed by an HTTP-level recording client, so the tests assert the exact JSON body and request headers that reach the Lettermint API.
The transport lives for the whole mailer lifetime (e.g. a queue worker) and drove the shared EmailEndpoint builder field by field. The SDK only resets that builder inside send(), so an error between the first builder call and send() left the tag, metadata, attachments and Idempotency-Key on the builder, and the next mail sent by that worker inherited them. doSend() now builds the complete payload as a local array and sends it through EmailEndpoint::send($payload) (available since lettermint-php 2.0.0). The idempotency key is set immediately before send(), which always clears it. The JSON body and request headers are unchanged for every existing case (covered by the characterisation tests). Attachments without a filename and emails without a subject are rejected with a TransportException before any request is made, instead of a TypeError (the API requires both fields).
The controller resolved the event type with WebhookEventType::from(), so any event type added to the API after the installed package version threw a ValueError and returned HTTP 500. Lettermint then retries the delivery and may disable the endpoint. Unknown, missing or non-string event names now return 200 and dispatch a new UnknownWebhookEventReceived event carrying the raw event name and the full verified payload. Known event types dispatch their typed events exactly as before.
…ery webhook event Laravel matches listeners on an event's own class and its interfaces, not parent classes, so listening on the abstract LettermintWebhookEvent never fired. Typed events and UnknownWebhookEventReceived now implement Contracts\WebhookEvent, and the README documents the working approach.
Require lettermint/lettermint-php ^3.0 and replace the 2.x SDK entry points. - The container binds one Lettermint\Lettermint client (singleton, alias "lettermint") built from the project token, the team token or both. Empty config values count as missing; with no token at all resolving the client throws ApiTokenNotFoundException::noTokens(). An optional Guzzle client can be bound under LettermintServiceProvider::HTTP_CLIENT. - The Lettermint facade resolves to that client and exposes its parts as methods: Lettermint::emails()->send(...), Lettermint::domains()->list(). - The mail transport sends each locally built payload with $client->emails->send($payload, idempotencyKey: $key) and maps every LettermintException to a TransportException whose code is the HTTP status (0 when no response was received). The request body is unchanged. - The team token is read from LETTERMINT_TEAM_TOKEN, falling back to LETTERMINT_API_TOKEN; the config keys are unchanged. The timeout is a float. - VerifyWebhookSignature verifies with the SDK 3.0 Webhook: the request HeaderBag is passed as is, X-Lettermint-Delivery is required, the 401 body carries the failure reason, and the request attribute holds a WebhookPayload instead of an array. BREAKING CHANGE: Requires lettermint/lettermint-php 3.0. The EmailEndpoint and ApiClient bindings, the lettermint.api alias, the empty Lettermint\Laravel\Lettermint class and TeamApiTokenNotFoundException are removed; the facade and the "lettermint" alias resolve to Lettermint\Lettermint. The transport constructor takes the client. Webhook deliveries without X-Lettermint-Delivery or a string event name are rejected with 401, and the verified payload attribute is a WebhookPayload.
Webhook data objects no longer throw when the API omits a field, adds one or sends a value of another type, so schema drift cannot turn a delivery into a 500 that Lettermint retries. - Every DTO reads its fields through an internal Field reader: a missing or mistyped value reads as null (or an empty list, or the documented default) instead of raising an error. - Fields the API may omit or send as null are nullable, for example subject, reason, score, spamScore, statusCode, linkIndex, the event dates and the scheduling timestamps. Identifiers the API always sends (messageId, recipient, route, suppression fields, envelope id) stay strings and read as '' if absent. - Inbound attachments delivered as signed URLs decode: EmailAttachment has url and expiresAt, and content is null for them. - Typed events carry the complete verified payload as $event->payload, so fields the DTOs do not map (context, sandbox, tags, ...) stay reachable. - A test pins WebhookEventType to the PHP SDK's WebhookEvent values. BREAKING CHANGE: Several webhook DTO properties are now nullable (see UPGRADE.md), WebhookEnvelope::$timestamp is nullable, and EmailAttachment::getDecodedContent() returns null for URL attachments.
Test the declared minimum, PHP 8.2, and the current PHP 8.5, besides 8.3 and 8.4. Skip the combinations Laravel does not support: Laravel 13 needs PHP 8.3 or later, and Laravel 10 and 11 do not support PHP 8.5.
Document the shared Lettermint client and facade, LETTERMINT_PROJECT_TOKEN and LETTERMINT_TEAM_TOKEN, the typed transport errors, queued-job and custom HTTP client usage, the required webhook headers and failure reasons, the tolerant webhook data and the raw event payload. UPGRADE.md covers every changed call with before/after examples, the nullable webhook data properties, the removed bindings and classes, and an instruction for upgrading with a coding agent. 2.x no longer receives updates, including fixes.
Bjornftw
approved these changes
Oct 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Laravel package 3.0, built on the new PHP SDK 3.0, following the same design as the other SDK majors.
What changes
lettermint/lettermint-php^3.0 (feat!: Lettermint PHP SDK 3.0 lettermint-php#43). The container holds one sharedLettermint\Lettermintclient; the facade exposesLettermint::emails(),::domains()and so on.UnknownWebhookEventReceived.Contracts\WebhookEventinterface lets one listener receive every event.$event->payloadholds the raw payload. This fixes the 2.x crash on inbound attachments sent as URLs.LETTERMINT_PROJECT_TOKEN/LETTERMINT_TEAM_TOKENare read, with the old env names as fallbacks. A timeout of0is now rejected.Verification
Release
Merge and release after
lettermint/lettermint-php3.0.0 (lettermint/lettermint-php#43) is on Packagist. Until then CI fails at dependency install. Afterwardscomposer updateresolves^3.0with no file changes. Then tag3.0.0.Docs for all SDK majors: lettermint/lettermint#2594 (stacked on lettermint/lettermint#2590). Merge it after the releases.
🤖 Generated with Claude Code