Skip to content
couryo

Developer guideIntegrations

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.

View as Markdown

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

  • The PHP SDK'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#

Terminal
composer require couryo/couryo-php

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

.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:

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

Done. Your existing code stays the same:

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:

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:

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:

.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.

Receive webhooks#

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:

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). 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
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