# Send email from Laravel

> Send email with Couryo from Laravel using Mail::to()->send(), through the PHP SDK's couryo transport or over SMTP, with tags, queues and a signed webhook.

Source: https://couryo.com/en/docs/laravel

Two ways, both with `Mail::to()->send()` and the Mailables you already have:

- **The [PHP SDK](https://couryo.com/en/docs/sdks.md)'s `couryo` transport** (recommended): talks to the API, returns the message ID and carries the Mailable's tags and metadata.
- **SMTP**: no new dependency, only `.env`.

## Option 1: SDK transport

```bash title="Terminal"
composer require couryo/couryo-php
```

The service provider registers itself (auto-discovery). Set `.env`:

```ini title=".env"
MAIL_MAILER=couryo
COURYO_API_KEY=ck_test_...
COURYO_WEBHOOK_SECRET=whsec_...
MAIL_FROM_ADDRESS=hello@example.com
MAIL_FROM_NAME="Example"
```

And add the mailer to `config/mail.php`:

```php title="config/mail.php"
'mailers' => [
    // ...
    'couryo' => [
        'transport' => 'couryo',
    ],
],
```

Done. Your existing code stays the same:

```php title="app/Http/Controllers/OrderController.php"
use App\Mail\OrderShipped;
use Illuminate\Support\Facades\Mail;

Mail::to($order->customer->email)->send(new OrderShipped($order));

// Queued (recommended, so the request does not wait):
Mail::to($order->customer->email)->queue(new OrderShipped($order));
```

The Mailable's tags and metadata become Couryo `tags` and `metadata`, for filtering in the dashboard and in webhooks:

```php title="app/Mail/OrderShipped.php"
use Illuminate\Mail\Mailables\Envelope;

public function envelope(): Envelope
{
    return new Envelope(
        subject: "Your order {$this->order->number} has shipped",
        tags: ['order'],
        metadata: ['order_id' => (string) $this->order->id],
    );
}
```

To send on the marketing stream (with one-click unsubscribe), add the `X-Couryo-Stream: marketing` header in the Mailable's `headers()` method. Without it, the email is transactional.

### Call the API directly

For what `Mail` does not cover (batch, saved templates, domains, contacts, events and campaigns), inject the client:

```php title="app/Http/Controllers/SignupController.php"
use Couryo\Couryo;

public function store(Request $request, Couryo $couryo)
{
    $user = User::create($request->validated());

    $couryo->events->track([
        'contact' => ['email' => $user->email, 'attributes' => ['name' => $user->name]],
        'name' => 'user.signed_up',
    ], ['idempotency_key' => "signup-{$user->id}"]);

    return redirect('/dashboard');
}
```

## Option 2: SMTP

No package at all. On Laravel 11 or newer:

```ini title=".env"
MAIL_MAILER=smtp
MAIL_SCHEME=smtp
MAIL_HOST=smtp.couryo.com
MAIL_PORT=587
MAIL_USERNAME=couryo
MAIL_PASSWORD=ck_live_...
MAIL_FROM_ADDRESS=hello@example.com
MAIL_FROM_NAME="Example"
```

On port 587 the connection upgrades to TLS (STARTTLS) on its own. For 465, use `MAIL_SCHEME=smtps`. On Laravel 10 or older, replace `MAIL_SCHEME` with `MAIL_ENCRYPTION=tls`.

Over SMTP, the headers `X-Couryo-Tags: kind=order` and `X-Couryo-Stream: marketing` on the Mailable play the role of tags and stream. See [SMTP](https://couryo.com/en/docs/smtp.md).

## Receive webhooks

```php title="routes/web.php"
use Couryo\Exception\WebhookVerificationError;
use Couryo\Webhook;
use Illuminate\Http\Request;

Route::post('/webhooks/couryo', function (Request $request) {
    try {
        $event = Webhook::verify(config('couryo.webhook_secret'), $request->headers->all(), $request->getContent());
    } catch (WebhookVerificationError $e) {
        abort(400, 'invalid signature');
    }

    if ($event['type'] === 'bounced') {
        // $event['data']['recipient'], $event['data']['explanation']
    }

    return response()->noContent();
});
```

Take the route out of CSRF protection (the signature already protects it). On Laravel 11 or newer:

```php title="bootstrap/app.php"
->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: ['webhooks/couryo']);
})
```

## Test without sending

With a `ck_test_` key, emails are captured and show up in the dashboard without reaching anyone ([test mode](https://couryo.com/en/docs/test-mode.md)). In automated tests, keep using `Mail::fake()`.

## Common problems

| Symptom | What to do |
|---|---|
| `Unsupported mail transport [couryo]` | the package was not loaded: run `composer dump-autoload` and `php artisan config:clear` |
| `domain_not_verified` | `MAIL_FROM_ADDRESS` must be on a [verified domain](https://couryo.com/en/docs/domains.md) |
| `535 5.7.8` over SMTP | username other than `couryo`, or a key that is invalid, revoked or without the `send` scope |
| Webhook returns `419` | the route is still under CSRF protection |
