Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ testbench.yaml
composer.lock
openapi.json
CLAUDE.md
AGENTS.md
.DS_Store
87 changes: 86 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,21 @@ composer require lettermint/lettermint-php

## Usage

Initialize the Lettermint client with your API token:
Initialize the Lettermint client with your Sending API token:

```php
$lettermint = new Lettermint\Lettermint('your-api-token');
```

For new integrations, prefer explicit clients for the two API surfaces:

```php
$email = Lettermint\Lettermint::email(getenv('LETTERMINT_SENDING_TOKEN'));
$api = Lettermint\Lettermint::api(getenv('LETTERMINT_API_TOKEN'));
```

Sending API tokens are project-specific and authenticate with the `x-lettermint-token` header. API tokens are team-scoped and authenticate with `Authorization: Bearer ...`. Keep these tokens separate and never reuse an API token for sending-only workloads.

### Sending Emails

The SDK provides a fluent interface for composing and sending emails:
Expand Down Expand Up @@ -63,6 +72,36 @@ $lettermint->email
->send();
```

You can also send with an array payload:

```php
$response = $email->send([
'from' => 'sender@example.com',
'to' => ['recipient@example.com'],
'subject' => 'Hello from Lettermint!',
'text' => 'Hello! This is a test email.',
]);
```

### Batch Sending

```php
$response = $email->sendBatch([
[
'from' => 'sender@example.com',
'to' => ['recipient@example.com'],
'subject' => 'First email',
'text' => 'Hello!',
],
[
'from' => 'sender@example.com',
'to' => ['another@example.com'],
'subject' => 'Second email',
'text' => 'Hello again!',
],
]);
```

#### Inline Attachments

You can embed images and other content in your HTML emails using content IDs:
Expand Down Expand Up @@ -96,6 +135,48 @@ same request with the same idempotency key, the API will return the same respons

For more information, refer to the [documentation](https://docs.lettermint.co/platform/emails/idempotency).

### API Client

Use the API client for team-scoped resources such as projects, domains, routes, suppressions, stats, messages, and webhooks:

```php
$api = Lettermint\Lettermint::api(getenv('LETTERMINT_API_TOKEN'));

$projects = $api->projects->list(['filter[search]' => 'production']);

$project = $api->projects->create([
'name' => 'Production',
'smtp_enabled' => false,
]);

$api->domains->verifyDnsRecords('domain-id');

$stats = $api->stats->retrieve([
'from' => '2026-05-01',
'to' => '2026-05-09',
]);

$api->suppressions->create([
'email' => 'user@example.com',
'reason' => 'manual',
'scope' => 'team',
]);

$api->webhooks->create([
'route_id' => 'route-id',
'name' => 'Production webhook',
'url' => 'https://example.com/lettermint/webhook',
'events' => ['message.sent', 'message.delivered'],
]);
```

Both API surfaces support `ping()`:

```php
$email->ping();
$api->ping();
```

## Testing

```bash
Expand All @@ -106,6 +187,10 @@ composer test

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

## Upgrading

Please see [UPGRADE.md](UPGRADE.md) for guidance on upgrading from v1 to v2.

## Contributing

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
Expand Down
156 changes: 156 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Upgrade Guide

## Upgrading from v1 to v2

Version 2 changes the PHP SDK response model for the sending API. The latest released v1 SDK only exposed email sending, so this guide focuses on migrating existing sending integrations.

### 1. Update Composer

```bash
composer require lettermint/lettermint-php:^2.0
```

### 2. Prefer the new email client entry point

The v1 constructor-based style still maps to the email endpoint, but v2 introduces a clearer sending entry point:

```php
$email = Lettermint\Lettermint::email($sendingToken);
```

Before:

```php
$lettermint = new Lettermint\Lettermint($sendingToken);

$response = $lettermint->email
->from('sender@example.com')
->to('recipient@example.com')
->subject('Hello')
->send();
```

After:

```php
$email = Lettermint\Lettermint::email($sendingToken);

$response = $email
->from('sender@example.com')
->to('recipient@example.com')
->subject('Hello')
->send();
```

Direct payload sending changes the same way:

```php
$response = $email->send([
'from' => 'sender@example.com',
'to' => ['recipient@example.com'],
'subject' => 'Hello',
]);
```

Batch sending:

```php
$response = $email->sendBatch([
[
'from' => 'sender@example.com',
'to' => ['recipient@example.com'],
'subject' => 'Hello',
],
]);
```

### 3. Update response handling

Sending responses are now typed resource objects with IDE autocomplete.

Before:

```php
$response = $lettermint->email->send();

$messageId = $response['message_id'];
$status = $response['status'];
```

After:

```php
$response = $email->send();

$messageId = $response->message_id;
$status = $response->status;
```

Array access is still available:

```php
$messageId = $response['message_id'];
```

Use `toArray()` when passing responses to existing array-based code:

```php
$payload = $response->toArray();
```

Batch responses are also typed:

```php
$response = $email->sendBatch($messages);

$firstMessageId = $response->data[0]->message_id;
```

To keep old array-style processing:

```php
$response = $email->sendBatch($messages)->toArray();

$firstMessageId = $response['data'][0]['message_id'];
```

### 4. Update ping checks

`ping()` now returns the raw API ping response as a string.

Before:

```php
if ($lettermint->email->ping() === 200) {
// Sending API reachable
}
```

After:

```php
if ($email->ping() === 'pong') {
// Sending API reachable
}
```

### 5. Search and replace checklist

Search your codebase for:

```text
new Lettermint\Lettermint(
->email
['message_id']
['status']
sendBatch(
ping() === 200
```

Then update response handling to use typed properties or `toArray()`.

### Notes

The main migration risk is code that assumes SDK responses are arrays. Most of that code can be migrated by either using property access or appending `->toArray()` at the SDK boundary.

Version 2 also adds a new full API client via `Lettermint::api($apiToken)`, but this is new functionality rather than a migration requirement from v1.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"analyse": "vendor/bin/phpstan analyse",
"test": "vendor/bin/pest",
"test-coverage": "vendor/bin/pest --coverage",
"format": "php-cs-fixer fix"
"format": "pint"
},
"config": {
"sort-packages": true,
Expand Down
70 changes: 70 additions & 0 deletions src/Client/ApiClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace Lettermint\Client;

use Lettermint\Client\Auth\TeamBearerTokenAuth;
use Lettermint\Endpoints\DomainsEndpoint;
use Lettermint\Endpoints\MessagesEndpoint;
use Lettermint\Endpoints\ProjectsEndpoint;
use Lettermint\Endpoints\RoutesEndpoint;
use Lettermint\Endpoints\StatsEndpoint;
use Lettermint\Endpoints\SuppressionsEndpoint;
use Lettermint\Endpoints\TeamEndpoint;
use Lettermint\Endpoints\WebhooksEndpoint;

/**
* @property-read DomainsEndpoint $domains Access domain operations.
* @property-read MessagesEndpoint $messages Access message operations.
* @property-read ProjectsEndpoint $projects Access project operations.
* @property-read RoutesEndpoint $routes Access route operations.
* @property-read StatsEndpoint $stats Access statistics operations.
* @property-read SuppressionsEndpoint $suppressions Access suppression operations.
* @property-read TeamEndpoint $team Access team operations.
* @property-read WebhooksEndpoint $webhooks Access webhook operations.
*/
class ApiClient
{
private HttpClient $httpClient;

private array $endpoints = [];

protected array $endpointRegistry = [
'domains' => DomainsEndpoint::class,
'messages' => MessagesEndpoint::class,
'projects' => ProjectsEndpoint::class,
'routes' => RoutesEndpoint::class,
'stats' => StatsEndpoint::class,
'suppressions' => SuppressionsEndpoint::class,
'team' => TeamEndpoint::class,
'webhooks' => WebhooksEndpoint::class,
];

public function __construct(string $apiToken, ?string $baseUrl = null)
{
$this->httpClient = new HttpClient(
new TeamBearerTokenAuth($apiToken),
$baseUrl ?? 'https://api.lettermint.co/v1'
);
}

public function __get($name)
{
if (isset($this->endpoints[$name])) {
return $this->endpoints[$name];
}

if (array_key_exists($name, $this->endpointRegistry)) {
$class = $this->endpointRegistry[$name];
$this->endpoints[$name] = new $class($this->httpClient);

return $this->endpoints[$name];
}

throw new \InvalidArgumentException("Unknown endpoint: $name");
}

public function ping(): string
{
return trim($this->httpClient->getRaw('/v1/ping'));
}
}
13 changes: 13 additions & 0 deletions src/Client/Auth/AuthStrategy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Lettermint\Client\Auth;

interface AuthStrategy
{
/**
* @return array<string, string>
*/
public function headers(): array;

public function token(): string;
}
18 changes: 18 additions & 0 deletions src/Client/Auth/SendingApiTokenAuth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Lettermint\Client\Auth;

class SendingApiTokenAuth implements AuthStrategy
{
public function __construct(private readonly string $apiToken) {}

public function headers(): array
{
return ['x-lettermint-token' => $this->apiToken];
}

public function token(): string
{
return $this->apiToken;
}
}
Loading
Loading