lettr / lettr-php
Lettr PHP SDK - Send emails via Lettr API
Installs: 126
Dependents: 1
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/lettr/lettr-php
Requires
- php: ^8.4
- guzzlehttp/guzzle: ^7.5
Requires (Dev)
- laravel/pint: ^1.18
- pestphp/pest: ^3.0
- phpstan/phpstan: ^2.0
README
Official PHP SDK for the Lettr email API.
Requirements
- PHP 8.4+
- Guzzle HTTP client 7.5+
Installation
composer require lettr/lettr-php
Quick Start
use Lettr\Lettr; $lettr = Lettr::client('your-api-key'); // Send an email $response = $lettr->emails()->send( $lettr->emails()->create() ->from('sender@example.com', 'Sender Name') ->to(['recipient@example.com']) ->subject('Hello from Lettr') ->html('<h1>Hello!</h1><p>This is a test email.</p>') ); echo $response->requestId; // Request ID for tracking echo $response->accepted; // Number of accepted recipients
Sending Emails
Using the Email Builder (Recommended)
The fluent builder provides a clean API for constructing emails:
$response = $lettr->emails()->send( $lettr->emails()->create() ->from('sender@example.com', 'Sender Name') ->to(['recipient@example.com']) ->cc(['cc@example.com']) ->bcc(['bcc@example.com']) ->replyTo('reply@example.com') ->subject('Welcome!') ->html('<h1>Welcome</h1>') ->text('Welcome (plain text fallback)') ->transactional() ->withClickTracking(true) ->withOpenTracking(true) ->metadata(['user_id' => '123', 'campaign' => 'welcome']) ->substitutionData(['name' => 'John', 'company' => 'Acme']) ->campaignId('welcome-series') );
Using SendEmailData DTO
For programmatic email construction:
use Lettr\Dto\Email\SendEmailData; use Lettr\Dto\Email\EmailOptions; use Lettr\ValueObjects\EmailAddress; use Lettr\ValueObjects\Subject; use Lettr\Collections\EmailAddressCollection; $email = new SendEmailData( from: new EmailAddress('sender@example.com', 'Sender'), to: EmailAddressCollection::from(['recipient@example.com']), subject: new Subject('Hello'), html: '<p>Email content</p>', ); $response = $lettr->emails()->send($email);
Quick Send Methods
For simple use cases:
// HTML email $response = $lettr->emails()->sendHtml( from: 'sender@example.com', to: 'recipient@example.com', subject: 'Hello', html: '<p>HTML content</p>', ); // Plain text email $response = $lettr->emails()->sendText( from: ['email' => 'sender@example.com', 'name' => 'Sender'], to: ['recipient1@example.com', 'recipient2@example.com'], subject: 'Hello', text: 'Plain text content', ); // Template email $response = $lettr->emails()->sendTemplate( from: 'sender@example.com', to: 'recipient@example.com', subject: 'Welcome!', templateSlug: 'welcome-email', templateVersion: 2, projectId: 123, substitutionData: ['name' => 'John'], );
Attachments
use Lettr\Dto\Email\Attachment; $email = $lettr->emails()->create() ->from('sender@example.com') ->to(['recipient@example.com']) ->subject('Document attached') ->html('<p>Please find the document attached.</p>') // From file path ->attachFile('/path/to/document.pdf') // With custom name and mime type ->attachFile('/path/to/file', 'custom-name.pdf', 'application/pdf') // From binary data ->attachData($binaryContent, 'report.csv', 'text/csv') // Using Attachment DTO ->attach(Attachment::fromFile('/path/to/image.png')); $response = $lettr->emails()->send($email);
Templates with Substitution Data
$response = $lettr->emails()->send( $lettr->emails()->create() ->from('sender@example.com') ->to(['recipient@example.com']) ->subject('Your Order #{{order_id}}') ->useTemplate('order-confirmation', version: 1, projectId: 123) ->substitutionData([ 'order_id' => '12345', 'customer_name' => 'John Doe', 'items' => [ ['name' => 'Product A', 'price' => 29.99], ['name' => 'Product B', 'price' => 49.99], ], 'total' => 79.98, ]) );
Email Options
$email = $lettr->emails()->create() ->from('sender@example.com') ->to(['recipient@example.com']) ->subject('Newsletter') ->html($htmlContent) // Tracking ->withClickTracking(true) ->withOpenTracking(true) // Mark as transactional (bypasses unsubscribe lists) ->transactional(false) // CSS inlining ->withInlineCss(true) // Template variable substitution ->withSubstitutions(true);
Retrieving Emails
Get Email Events by Request ID
use Lettr\ValueObjects\RequestId; // After sending $response = $lettr->emails()->send($email); $requestId = $response->requestId; // Later, retrieve events $result = $lettr->emails()->get($requestId); // or $result = $lettr->emails()->get('req_abc123'); foreach ($result->events as $event) { echo $event->type->value; // 'delivery', 'open', 'click', etc. echo $event->recipient; // Recipient email echo $event->timestamp; // When the event occurred echo $event->messageId; // Unique message ID // Event-specific data if ($event->type === EventType::Click) { echo $event->clickUrl; // Clicked URL } if ($event->type === EventType::Bounce) { echo $event->bounceClass; // Bounce classification echo $event->reason; // Bounce reason } }
List Email Events with Filtering
use Lettr\Dto\Email\ListEmailsFilter; // List all events $result = $lettr->emails()->list(); // With filters $filter = ListEmailsFilter::create() ->perPage(50) ->forRecipient('user@example.com') ->fromDate('2024-01-01') ->toDate('2024-12-31'); $result = $lettr->emails()->list($filter); echo $result->totalCount; echo $result->pagination->hasNextPage(); // Paginate through results while ($result->hasMore()) { foreach ($result->events as $event) { // Process event } $filter = $filter->cursor($result->pagination->nextCursor); $result = $lettr->emails()->list($filter); }
Domain Management
List Domains
$domains = $lettr->domains()->list(); foreach ($domains as $domain) { echo $domain->domain; // example.com echo $domain->status->value; // 'pending', 'approved' echo $domain->canSend; // true/false echo $domain->dkimStatus; // DnsStatus enum echo $domain->returnPathStatus; // DnsStatus enum }
Add a Domain
use Lettr\ValueObjects\DomainName; $result = $lettr->domains()->create('example.com'); // or $result = $lettr->domains()->create(new DomainName('example.com')); echo $result->domain; echo $result->status; // DNS records to configure echo $result->dns->returnPathHost; // Return path CNAME host echo $result->dns->returnPathValue; // Return path CNAME value if ($result->dns->dkim !== null) { echo $result->dns->dkim->selector; // DKIM selector echo $result->dns->dkim->publicKey; // DKIM public key }
Get Domain Details
$domain = $lettr->domains()->get('example.com'); echo $domain->domain; echo $domain->status; echo $domain->canSend; echo $domain->trackingDomain; echo $domain->createdAt; echo $domain->verifiedAt; // DNS configuration echo $domain->dns->returnPathHost; echo $domain->dns->returnPathValue;
Verify Domain DNS
$verification = $lettr->domains()->verify('example.com'); if ($verification->isFullyVerified()) { echo "Domain is ready to send!"; } else { // Check individual records if (!$verification->dkim->isValid()) { echo "DKIM error: " . $verification->dkim->error; echo "Expected: " . $verification->dkim->expected; echo "Found: " . $verification->dkim->found; } if (!$verification->returnPath->isValid()) { echo "Return path error: " . $verification->returnPath->error; } }
Delete a Domain
$lettr->domains()->delete('example.com');
Webhooks
List Webhooks
$webhooks = $lettr->webhooks()->list(); foreach ($webhooks as $webhook) { echo $webhook->id; echo $webhook->name; echo $webhook->url; echo $webhook->enabled; echo $webhook->authType->value; // 'none', 'basic', 'bearer' // Event types this webhook listens to foreach ($webhook->eventTypes as $eventType) { echo $eventType->value; // 'delivery', 'bounce', 'open', etc. } // Health check if ($webhook->isFailing()) { echo "Last error: " . $webhook->lastError; } }
Get Webhook Details
$webhook = $lettr->webhooks()->get('webhook-id'); echo $webhook->name; echo $webhook->url; echo $webhook->lastStatus?->value; echo $webhook->lastTriggeredAt; // Check if webhook listens to specific events if ($webhook->listensTo(EventType::Bounce)) { echo "Webhook receives bounce notifications"; }
Templates
List Templates
use Lettr\Dto\Template\ListTemplatesFilter; // List all templates $response = $lettr->templates()->list(); foreach ($response->templates as $template) { echo $template->id; echo $template->name; echo $template->slug; echo $template->projectId; } // With pagination $filter = ListTemplatesFilter::create() ->projectId(123) ->perPage(20) ->page(2); $response = $lettr->templates()->list($filter);
Get Template Details
$template = $lettr->templates()->get('welcome-email'); echo $template->id; echo $template->name; echo $template->slug; echo $template->html; echo $template->json; echo $template->activeVersion; echo $template->versionsCount; // With specific project $template = $lettr->templates()->get('welcome-email', projectId: 123);
Create a Template
use Lettr\Dto\Template\CreateTemplateData; $template = $lettr->templates()->create(new CreateTemplateData( name: 'My Template', slug: 'my-template', // optional, auto-generated if not provided projectId: 123, // optional folderId: 5, // optional html: '<html>...</html>', // optional json: '{"blocks":[]}', // optional, TOPOL.io JSON format )); echo $template->id; echo $template->name; echo $template->slug; echo $template->projectId; echo $template->folderId; echo $template->activeVersion; // Merge tags extracted from the template foreach ($template->mergeTags as $tag) { echo $tag->key; echo $tag->required; }
Delete a Template
$lettr->templates()->delete('my-template'); // With specific project $lettr->templates()->delete('my-template', projectId: 123);
Get Merge Tags
Retrieve merge tags (template variables) from a template:
$response = $lettr->templates()->getMergeTags('welcome-email'); echo $response->projectId; echo $response->templateSlug; echo $response->version; foreach ($response->mergeTags as $tag) { echo $tag->key; // e.g., 'user_name' echo $tag->required; // true/false echo $tag->type; // e.g., 'string', 'object' // Nested tags (for objects) if ($tag->children !== null) { foreach ($tag->children as $child) { echo $child->key; // e.g., 'first_name' echo $child->type; // e.g., 'string' } } } // With specific project and version $response = $lettr->templates()->getMergeTags( 'welcome-email', projectId: 123, version: 2, );
Health Check
$status = $lettr->health()->check(); echo $status->status; // 'ok' echo $status->timestamp; // ISO 8601 timestamp echo $status->isHealthy(); // true/false
Event Types
The SDK provides an EventType enum with helper methods:
use Lettr\Enums\EventType; $type = EventType::Delivery; $type->label(); // "Delivery" $type->isSuccess(); // true (injection, delivery) $type->isFailure(); // false (bounce, policy_rejection, etc.) $type->isEngagement(); // false (open, initial_open, click) $type->isUnsubscribe(); // false (list_unsubscribe, link_unsubscribe)
Available event types:
injection- Email accepted for deliverydelivery- Email delivered to recipientbounce- Email bounceddelay- Delivery delayedpolicy_rejection- Rejected by policyout_of_band- Out of band bounceopen- Email openedinitial_open- First openclick- Link clickedgeneration_failure- Template generation failedgeneration_rejection- Template generation rejectedspam_complaint- Marked as spamlist_unsubscribe- Unsubscribed via list headerlink_unsubscribe- Unsubscribed via link
Value Objects
The SDK uses value objects for type safety and validation:
use Lettr\ValueObjects\EmailAddress; use Lettr\ValueObjects\DomainName; use Lettr\ValueObjects\RequestId; use Lettr\ValueObjects\Timestamp; // Email addresses with optional name $email = new EmailAddress('user@example.com', 'User Name'); echo $email->address; // user@example.com echo $email->name; // User Name // Domain names (validated) $domain = new DomainName('example.com'); // Request IDs $requestId = new RequestId('req_abc123'); // Timestamps $timestamp = Timestamp::fromString('2024-01-15T10:30:00Z'); echo $timestamp->toIso8601(); echo $timestamp->toDateTime();
Error Handling
use Lettr\Exceptions\ApiException; use Lettr\Exceptions\TransporterException; use Lettr\Exceptions\ValidationException; use Lettr\Exceptions\NotFoundException; use Lettr\Exceptions\UnauthorizedException; use Lettr\Exceptions\ConflictException; use Lettr\Exceptions\InvalidValueException; try { $response = $lettr->emails()->send($email); } catch (ValidationException $e) { // Invalid request data (422) echo "Validation failed: " . $e->getMessage(); } catch (UnauthorizedException $e) { // Invalid API key (401) echo "Authentication failed: " . $e->getMessage(); } catch (NotFoundException $e) { // Resource not found (404) echo "Not found: " . $e->getMessage(); } catch (ConflictException $e) { // Resource conflict (409) echo "Conflict: " . $e->getMessage(); } catch (ApiException $e) { // Other API errors echo "API error ({$e->getCode()}): " . $e->getMessage(); } catch (TransporterException $e) { // Network/transport errors echo "Network error: " . $e->getMessage(); } catch (InvalidValueException $e) { // Invalid value object (e.g., invalid email format) echo "Invalid value: " . $e->getMessage(); }
Development
Install Dependencies
composer install
Code Style
This project uses Laravel Pint for code style:
composer lint
Static Analysis
This project uses PHPStan at level 8:
composer analyse
Testing
This project uses Pest for testing:
composer test
Contributing
Please see CONTRIBUTING for details.
License
MIT License. See LICENSE for details.