The Lettr PHP SDK offers multiple ways to send emails, from quick one-liners to the full-featured email builder. This page covers every approach so you can choose the one that fits your use case.
| Approach | Best For |
|---|
| Quick Send Methods | Simple emails with minimal configuration |
| Email Builder | Complex emails with tracking, metadata, and multiple recipients |
| Templates | Lettr-managed templates with dynamic data |
| Attachments | Sending files alongside email content |
Quick Send Methods
For simple emails, use the shorthand methods. These are convenience wrappers around the email builder that handle the most common use cases with minimal code.
HTML Email
$response = $lettr->emails()->sendHtml(
from: 'sender@yourdomain.com',
to: 'recipient@example.com',
subject: 'Hello',
html: '<h1>Welcome!</h1><p>Thanks for signing up.</p>',
);
The sendHtml() method is perfect for simple HTML emails where you don’t need advanced features like tracking configuration or metadata.
When providing HTML content, the SDK automatically enables CSS inlining and template substitutions by default. You can disable these behaviors using the email builder for more control.
Plain Text Email
use Lettr\ValueObjects\EmailAddress;
$response = $lettr->emails()->sendText(
from: new EmailAddress('sender@yourdomain.com', 'Your App'),
to: ['user1@example.com', 'user2@example.com'],
subject: 'Plain text update',
text: 'This is a plain text email.',
);
The from parameter accepts either a string email address or an EmailAddress value object to set a display name. The to parameter accepts a single email string or an array of recipients (maximum 50).
You can send both HTML and plain text in the same email by using the email builder with both ->html() and ->text() methods. Mail clients that don’t support HTML will display the plain text version.
Template Email
$response = $lettr->emails()->sendTemplate(
from: 'sender@yourdomain.com',
to: 'customer@example.com',
subject: 'Welcome!',
templateSlug: 'welcome-email',
substitutionData: ['name' => 'John'],
);
The substitutionData array keys correspond to merge tags in your Lettr template (e.g., {{name}}). See Merge Tags & Template Language for the full syntax including conditionals and loops.
For more advanced template usage including versioning, see the Templates page.
Email Builder
For complex emails that require multiple features, use the fluent email builder. This gives you access to every option the Lettr API supports in a chainable interface:
$response = $lettr->emails()->send(
$lettr->emails()->create()
->from('sender@yourdomain.com', 'Your Company')
->to(['recipient@example.com'])
->cc(['cc@example.com'])
->bcc(['bcc@example.com'])
->replyTo('reply@yourdomain.com')
->subject('Monthly Newsletter')
->html('<h1>Newsletter</h1><p>{{content}}</p>')
->text('Newsletter: {{content}}')
->substitutionData(['content' => $newsletterContent])
->withOpenTracking(true)
->withClickTracking(true)
->tag('welcome')
->metadata(['source' => 'website'])
);
// Access the response
echo $response->requestId;
echo $response->accepted;
The builder is useful when you need to combine multiple features in a single email — for example, HTML content with a plain text fallback, tracking configuration, metadata for analytics, and attachments.
All builder methods return the builder instance, so you can chain as many method calls as needed. The order of method calls doesn’t matter — the builder collects all the data and sends it to the API when you call send().
Templates with Substitution Data
Send using a Lettr template with dynamic data:
$response = $lettr->emails()->send(
$lettr->emails()->create()
->from('sender@yourdomain.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,
])
);
The useTemplate() method accepts an optional version parameter to pin a specific template version. See Templates for details.
When to use versioning:
- Production safety — Pin to a known-good version so publishing a new draft doesn’t affect live emails
- A/B testing — Send different versions to different cohorts and compare metrics
- Gradual rollout — Test a new version with a subset of users before publishing
Attachments
Add attachments from file paths or binary data:
$response = $lettr->emails()->send(
$lettr->emails()->create()
->from('billing@yourdomain.com')
->to(['customer@example.com'])
->subject('Your Invoice')
->html('<p>Please find your invoice attached.</p>')
// From file path
->attachFile('/path/to/invoice.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')
);
When attaching files, the SDK automatically:
- Base64-encodes the file content
- Detects the MIME type if not provided
- Validates file size and format
Large attachments increase email size and can affect deliverability. Keep attachments under 10 MB total per email. For larger files, consider uploading to cloud storage and including a download link in the email instead.
Email Options
Fine-tune email behavior with tracking and processing options:
$email = $lettr->emails()->create()
->from('sender@yourdomain.com')
->to(['recipient@example.com'])
->subject('Newsletter')
->html($htmlContent)
// Tracking
->withClickTracking(true)
->withOpenTracking(true)
// Mark as transactional (bypasses unsubscribe lists)
->transactional()
// CSS inlining
->withInlineCss(true)
// Template variable substitution in HTML
->withSubstitutions(true);
$response = $lettr->emails()->send($email);
| Option | Default | Description |
|---|
withClickTracking() | true | Track link clicks in the email |
withOpenTracking() | true | Track when recipients open the email |
transactional() | true | Mark as transactional (bypasses unsubscribe lists) |
withInlineCss() | false | Automatically inline CSS styles for better email client compatibility |
withSubstitutions() | true | Enable merge tag substitution in HTML/text content |
Click and open tracking are enabled by default. For transactional emails like password resets or order confirmations, leave tracking enabled — the data helps you diagnose delivery issues. For privacy-sensitive communications, disable tracking explicitly.
Handling Responses
Every send method returns a SendEmailResponse with details about the request:
$response = $lettr->emails()->send($email);
$response->requestId; // Request ID for tracking
$response->accepted; // Number of accepted recipients
$response->rejected; // Number of rejected recipients
$response->allAccepted(); // true if all recipients accepted
$response->hasRejections(); // true if any were rejected
$response->total(); // Total recipients (accepted + rejected)
The requestId is a unique identifier for this email transmission. Store it in your database to correlate emails with delivery events later. You can use it to query the Lettr API for detailed delivery information including opens, clicks, bounces, and complaints.
Save the requestId in your application’s database alongside the user record or transaction. This lets you look up delivery status, debug issues, and provide customer support without searching through logs.
Error Handling
The SDK throws typed exceptions for different API errors, making it easy to handle specific failure cases:
use Lettr\Exceptions\ValidationException;
use Lettr\Exceptions\UnauthorizedException;
use Lettr\Exceptions\NotFoundException;
use Lettr\Exceptions\ConflictException;
use Lettr\Exceptions\QuotaExceededException;
use Lettr\Exceptions\RateLimitException;
use Lettr\Exceptions\ApiException;
use Lettr\Exceptions\TransporterException;
try {
$response = $lettr->emails()->send($email);
} catch (ValidationException $e) {
// Invalid request data (422)
// e.g., unverified sender domain, invalid email format
} catch (UnauthorizedException $e) {
// Invalid API key (401)
} catch (NotFoundException $e) {
// Resource not found (404)
// e.g., template slug doesn't exist
} catch (ConflictException $e) {
// Resource conflict (409)
} catch (QuotaExceededException $e) {
// Sending quota exceeded (429) — monthly or daily limit reached
// $e->quota contains quota details (free tier only)
} catch (RateLimitException $e) {
// API rate limit exceeded (429) — too many requests per second
// $e->rateLimit contains limit/remaining/reset info
// $e->retryAfter contains seconds to wait
} catch (ApiException $e) {
// Other API errors (server errors, etc.)
} catch (TransporterException $e) {
// Network/transport errors (connection failures, timeouts)
}
| Exception | HTTP Status | Common Causes |
|---|
ValidationException | 422 | Invalid email format, unverified sending domain, missing required fields |
UnauthorizedException | 401 | Invalid or expired API key |
NotFoundException | 404 | Template slug doesn’t exist, invalid project ID |
ConflictException | 409 | Resource already exists or conflicts with current state |
QuotaExceededException | 429 | Monthly or daily sending limit reached (free tier) |
RateLimitException | 429 | Too many requests per second (3 req/s per team) |
ApiException | 4xx/5xx | Server errors, other API errors |
TransporterException | N/A | Network failures, timeouts, DNS errors |
Always wrap email sending in a try-catch block in production. Network failures and API errors can happen at any time, and unhandled exceptions will crash your application.
What’s Next