# PHP

Use the official [Lettermint PHP SDK](https://github.com/lettermint/lettermint-php) to send email from a PHP application. If you use Laravel, use the [Laravel mail driver](/guides/send-email-with-laravel). If you use Symfony Mailer, use the [Symfony guide](/guides/send-email-with-symfony). See [SDKs and integrations](/sdks) for other packages.

:::info
This guide covers SDK 3.0. If your project uses 2.x, follow the [PHP SDK 3.0 upgrade guide](https://github.com/lettermint/lettermint-php/blob/main/UPGRADE.md) on GitHub.
:::

## Requirements

Before you start you need:

- PHP 8.2 or newer with Composer. The SDK uses Guzzle 7.15.2 or newer, or Guzzle 8, which Composer installs for you.
- A [Lettermint account](https://app.lettermint.co) with a verified sending domain
- A Project API token from your [project settings](https://app.lettermint.co/projects)

If your domain is not verified yet, follow the [domain setup guide](/platform/domains/introduction) first so your mail reaches the inbox instead of the spam folder.

:::tip
Building a Laravel app? Use the [Laravel guide](/guides/send-email-with-laravel) instead. It wraps this SDK in a mail driver that works with Laravel's `Mail` facade, Mailables, and queues.
:::

## 1. Installation

Install the SDK via Composer:

```bash
composer require lettermint/lettermint-php
```

## 2. Send your first email

Create the client with your Project API token as the `sendingToken`:

```php
<?php

require_once 'vendor/autoload.php';

use Lettermint\Lettermint;

$lettermint = new Lettermint(sendingToken: getenv('LETTERMINT_PROJECT_TOKEN'));
```

The client stores nothing about a message, so create it once and share it across your app, also in long-running workers such as Octane, Horizon, or Symfony Messenger. You can also pass the token as a string, as in `new Lettermint($token)`.

Send your first email:

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

echo "Email sent with ID: " . $response->message_id;
```

`emails->send()` takes the message in the API's format, for example `reply_to` and `scheduled_at`. The `to`, `cc`, and `bcc` fields are arrays.

:::note
The Team API uses a separate Team API token. Pass it as `teamToken`, alone or next to `sendingToken`. Each part of the client uses its own token and never falls back to the other. See the [Team API quickstart](/platform/teams/team-api/quickstart).
:::

## 3. Email features

The examples in this section use `$lettermint->emails->compose()`, a fluent builder. Every setter returns a new builder and leaves the original unchanged, so one builder is safe to share and reuse. Keep the value that a setter returns when you build an email over several statements:

```php
$draft = $lettermint->emails->compose()
    ->from('John Doe <john@yourdomain.com>')
    ->to('recipient@example.com')
    ->subject('Your invoice');

if ($copyAccountant) {
    $draft = $draft->cc('accountant@yourdomain.com');
}

$draft->html('<p>Your invoice is ready.</p>')->send();
```

Each builder example is equivalent to an array passed to `$lettermint->emails->send()`. Use whichever reads better.

### Basic email

Send a simple text or HTML email:

```php
$lettermint->emails->compose()
    ->from('John Doe <john@yourdomain.com>')
    ->to('recipient@example.com')
    ->subject('Your account is ready!')
    ->html('<h1>Welcome!</h1><p>Thanks for signing up.</p>')
    ->text('Welcome! Thanks for signing up.')
    ->send();
```

### Multiple recipients

Send to multiple recipients using CC and BCC:

```php
$lettermint->emails->compose()
    ->from('John Doe <john@yourdomain.com>')
    ->to('user1@example.com', 'user2@example.com')
    ->cc('manager@yourdomain.com')
    ->bcc('archive@yourdomain.com')
    ->subject('Monthly Newsletter')
    ->html('<h1>This Month\'s Updates</h1>')
    ->send();
```

### Custom headers and reply-to

Add custom headers and set reply-to addresses:

```php
$lettermint->emails->compose()
    ->from('support@yourdomain.com')
    ->to('customer@example.com')
    ->replyTo('help@yourdomain.com')
    ->subject('Support Ticket #12345')
    ->headers([
        'X-Priority' => '1',
        'X-Ticket-ID' => '12345'
    ])
    ->html('<p>Your support ticket has been updated.</p>')
    ->send();
```

### Metadata

Add metadata for tracking and webhook payloads:

```php
$lettermint->emails->compose()
    ->from('notifications@yourdomain.com')
    ->to('user@example.com')
    ->subject('Order Confirmation')
    ->metadata([
        'order_id' => '12345',
        'customer_id' => 'cust_789',
        'campaign' => 'order_confirmation'
    ])
    ->html('<p>Your order has been confirmed.</p>')
    ->send();
```

:::note
Metadata is included in webhook payloads but not added to the actual email headers. Use it for tracking and analytics purposes.
:::

### Tags

Categorize emails for filtering and analytics:

```php
$lettermint->emails->compose()
    ->from('alerts@yourdomain.com')
    ->to('admin@example.com')
    ->subject('System Alert')
    ->tags([
        ['name' => 'category', 'value' => 'system-alerts'],
        ['name' => 'environment', 'value' => 'production'],
    ])
    ->html('<p>Critical system alert detected.</p>')
    ->send();
```

:::note
`tags()` accepts up to 20 name and value pairs. The SDK checks names and values before it sends the request and throws `LettermintValidationException` if one is invalid. `tag()` still sets the existing singular tag, which has no deprecation date. See [Tags documentation](/platform/emails/tags).
:::

### Route selection

Direct emails to specific routes within your project:

```php
$lettermint->emails->compose()
    ->from('notifications@yourdomain.com')
    ->to('user@example.com')
    ->subject('Welcome!')
    ->route('transactional')
    ->html('<p>Welcome to our platform.</p>')
    ->send();
```

### Scheduling

Deliver an email later with `scheduledAt()`. It takes a `DateTimeInterface`, an ISO 8601 string, or English such as `tomorrow 9am`:

```php
$result = $lettermint->emails->compose()
    ->from('notifications@yourdomain.com')
    ->to('user@example.com')
    ->subject('Your trial ends tomorrow')
    ->text('Your trial ends tomorrow.')
    ->scheduledAt(new DateTimeImmutable('+1 day'))
    ->send();
```

See [Scheduling](/platform/emails/scheduling) for rescheduling and cancelling.

### Idempotency

Pass an idempotency key so a retry never sends the same email twice. If a request with the same key is repeated within the idempotency window, Lettermint returns the original result instead of sending again. The key is an argument of the call that sends the email:

```php
$lettermint->emails->compose()
    ->from('notifications@yourdomain.com')
    ->to('user@example.com')
    ->subject('Order Confirmation')
    ->html('<p>Your order has been confirmed.</p>')
    ->send(idempotencyKey: 'order-confirmation-12345');
```

`emails->send($message, idempotencyKey: …)` and `emails->sendBatch($messages, idempotencyKey: …)` take the same argument. The key applies only to that call, and the SDK never retries on its own.

:::tip
Derive the key from something stable in your domain, such as an order or invoice ID, so the same business event always maps to the same key. See the [idempotency documentation](/platform/emails/idempotency) for the full behaviour.
:::

### File attachments

Attach files to your emails. `Attachment` takes raw bytes and base64-encodes them for you:

```php
use Lettermint\Attachment;

$lettermint->emails->compose()
    ->from('invoices@yourdomain.com')
    ->to('customer@example.com')
    ->subject('Your Invoice')
    ->html('<p>Please find your invoice attached.</p>')
    ->attach(Attachment::fromPath('/path/to/document.pdf', filename: 'invoice.pdf', contentType: 'application/pdf'))
    ->send();
```

Use `new Attachment($filename, $bytes, $contentType)` for content in memory, or `Attachment::fromBase64()` for content that is already base64. To show an attachment inside the HTML body, set `contentId` and reference it as `cid:` in the markup. See [Inline images](/platform/emails/inline-images).

### Batch sending

Send up to 500 emails in one request. The list can mix arrays and builders, and the result has one `SendMailResponse` per email, in order:

```php
$base = $lettermint->emails->compose()
    ->from('John Doe <john@yourdomain.com>')
    ->subject('Your weekly summary');

$results = $lettermint->emails->sendBatch([
    ['from' => 'John Doe <john@yourdomain.com>', 'to' => ['user1@example.com'], 'subject' => 'Hi', 'text' => 'Hello'],
    $base->to('user2@example.com')->text('Your summary is ready.'),
], idempotencyKey: 'weekly-summary-2026-10-05');

echo $results[0]->message_id;
```

## 4. Send an order confirmation

A real transactional send usually pulls several of these features together. This example sends a receipt with an HTML and plain-text body, tags it for analytics, attaches the order and customer IDs as metadata, and uses an idempotency key so a retry never emails the customer twice:

```php title="order-confirmation.php"
<?php

require_once 'vendor/autoload.php';

use Lettermint\Lettermint;

$lettermint = new Lettermint(sendingToken: getenv('LETTERMINT_PROJECT_TOKEN'));

// $order comes from your application (database, cart, etc.)
$order = [
    'id' => 'ORD-12345',
    'number' => '12345',
    'total' => '€49.00',
    'customer_email' => 'customer@example.com',
    'customer_id' => 'cust_789',
];

$response = $lettermint->emails->compose()
    ->from('Acme Store <orders@yourdomain.com>')
    ->to($order['customer_email'])
    ->replyTo('support@yourdomain.com')
    ->subject("Order {$order['number']} confirmed")
    ->html("<h1>Thanks for your order</h1><p>Order {$order['number']} totalling {$order['total']} is confirmed.</p>")
    ->text("Thanks for your order. Order {$order['number']} totalling {$order['total']} is confirmed.")
    ->tags([['name' => 'type', 'value' => 'order-confirmation']])
    ->metadata([
        'order_id' => $order['id'],
        'customer_id' => $order['customer_id'],
    ])
    ->route('transactional')
    ->send(idempotencyKey: "order-confirmation-{$order['id']}");

echo "Order confirmation sent: " . $response->message_id;
```

The `metadata` you attach here travels with every webhook event for the message, so a later delivery or bounce maps straight back to the order in your database.

## 5. Response

```php
$response = $lettermint->emails->compose()
    ->from('John Doe <john@yourdomain.com>')
    ->to('recipient@example.com')
    ->subject('Test')
    ->text('Hello!')
    ->send();

echo $response->message_id; // Email ID
echo $response->status;     // Current status
```

## 6. Handle errors

Every exception the SDK throws extends `Lettermint\Exceptions\LettermintException`. API errors extend `ApiException`, which carries `status`, `errorCode`, and the message. Catch the subclasses you want to handle:

```php
use Lettermint\Exceptions\ApiException;
use Lettermint\Exceptions\RateLimitException;
use Lettermint\Exceptions\TimeoutException;
use Lettermint\Exceptions\ValidationException;

try {
    $lettermint->emails->send($message, idempotencyKey: $key);
} catch (ValidationException $e) {
    error_log($e->getMessage() . ' ' . json_encode($e->errors));
} catch (RateLimitException $e) {
    sleep($e->retryAfter ?? 1); // then retry with the same idempotency key
} catch (TimeoutException $e) {
    // The outcome is unknown. Retry with the same idempotency key.
} catch (ApiException $e) {
    error_log("{$e->status} {$e->errorCode} {$e->getMessage()}");
}
```

The SDK never retries on its own. Reuse the same idempotency key when you retry so the customer gets one email. Other exceptions include `AuthenticationException`, `PermissionException`, `NotFoundException`, `ConflictException`, `ServerException`, `ConnectionException`, `LettermintConfigException` for a missing token, and `LettermintValidationException` when the SDK rejects a request before it sends it.

## Track delivery, opens, and bounces

Sending is only half of a transactional setup. To see what happens after a message leaves your app, combine the SDK with Lettermint's platform features:

- [Email tracking](/platform/emails/tracking/introduction) records opens and clicks, with bot filtering so your metrics stay accurate.
- [Webhooks](/platform/webhooks/introduction) push delivery, bounce, and complaint events to your server in real time, so you can update order records or suppress bad addresses. The SDK verifies each delivery with `(new Webhook($secret))->verify($rawBody, $headers)`. See [Webhook signatures](/platform/webhooks/signing#verify-with-the-sdks).
- [Test addresses](/platform/emails/sending-test-emails) let you trigger a hard or soft bounce on demand while you build and verify your webhook handler.

Tracking is configured per route, so turn it on for the route your app sends through, then read the results from your dashboard or your webhook endpoint.

## FAQ

<details>
<summary>Does the SDK work with Laravel or another framework?</summary>

The SDK works in any PHP project, including Symfony and plain PHP. If you use Laravel, install the dedicated [Laravel mail driver](/guides/send-email-with-laravel) instead, which integrates with the `Mail` facade, Mailables, and queues rather than calling the SDK directly.

</details>

<details>
<summary>Which PHP versions are supported?</summary>

PHP 8.2 or newer. Install the SDK with a single `composer require lettermint/lettermint-php`. CI runs the SDK on PHP 8.2 to 8.5.

</details>

<details>
<summary>How do I stop the same email being sent twice?</summary>

Pass an `idempotencyKey` argument to `send()` or `sendBatch()`, derived from a stable ID such as an order number. A repeated request with the same key returns the original result instead of sending again. See the [idempotency documentation](/platform/emails/idempotency).

</details>

<details>
<summary>Is Lettermint email GDPR compliant and EU-hosted?</summary>

Yes. Lettermint runs exclusively on European infrastructure and processes email in line with GDPR, so transactional mail from your PHP app is handled inside the EU.

</details>

<details>
<summary>How do I track opens, clicks, and bounces?</summary>

Enable [tracking](/platform/emails/tracking/introduction) on your route and subscribe to [webhooks](/platform/webhooks/introduction) to receive delivery, open, and bounce events. Use the [test addresses](/platform/emails/sending-test-emails) to simulate bounces while you build your handler.

</details>

## Next steps

<CardGroup cols={2}>
    <Card title="Tags" icon="tag" href="/platform/emails/tags">
        Organize and filter emails with tags.
    </Card>
    <Card title="Tracking" icon="chart-line" href="/platform/emails/tracking/introduction">
        Track opens, clicks, and deliverability.
    </Card>
    <Card title="Webhooks" icon="webhook" href="/platform/webhooks/introduction">
        Receive delivery events through webhooks.
    </Card>
    <Card title="Laravel Driver" icon="code" href="/guides/send-email-with-laravel">
        Use the dedicated Lettermint mail driver for Laravel.
    </Card>
</CardGroup>

<Card title="GitHub Repository" icon="github" href="https://github.com/lettermint/lettermint-php">
    Find the complete source code, report issues, or contribute on GitHub.
</Card>
