Laravel

Send transactional emails from your Laravel application using the Veil Mail PHP SDK.

Prerequisites

1. Install the SDK

composer require veilmail/veilmail-php

2. Configure the Client

Add your credentials to .env and create a service class for the client:

.env
VEILMAIL_API_KEY=veil_live_xxxxx
VEILMAIL_WEBHOOK_SECRET=whsec_xxxxx
app/Services/VeilMailService.php
<?php

namespace App\Services;

use VeilMail\VeilMail;

class VeilMailService
{
    private VeilMail $client;

    public function __construct()
    {
        $this->client = new VeilMail(config('services.veilmail.key'));
    }

    public function client(): VeilMail
    {
        return $this->client;
    }
}
config/services.php
// config/services.php
return [
    // ...
    'veilmail' => [
        'key' => env('VEILMAIL_API_KEY'),
        'webhook_secret' => env('VEILMAIL_WEBHOOK_SECRET'),
    ],
];

Register the service in your AppServiceProvider:

app/Providers/AppServiceProvider.php
// app/Providers/AppServiceProvider.php
use App\Services\VeilMailService;

public function register(): void
{
    $this->app->singleton(VeilMailService::class);
}

3. Send Your First Email

Add a controller action that sends a transactional email:

app/Http/Controllers/EmailController.php
<?php

namespace App\Http\Controllers;

use App\Services\VeilMailService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class EmailController extends Controller
{
    public function __construct(
        private readonly VeilMailService $veilmail,
    ) {}

    public function sendWelcome(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'email' => 'required|email',
            'name' => 'required|string',
        ]);

        $result = $this->veilmail->client()->emails->send([
            'from' => 'hello@yourdomain.com',
            'to' => $validated['email'],
            'subject' => "Welcome, {$validated['name']}!",
            'html' => "<h1>Welcome</h1><p>Hi {$validated['name']}, thanks for joining.</p>",
        ]);

        return response()->json(['emailId' => $result['id']]);
    }
}
routes/api.php
// routes/api.php
use App\Http\Controllers\EmailController;

Route::post('/send-welcome', [EmailController::class, 'sendWelcome']);

4. Send with a Template

Use templates to manage email content outside your code:

app/Http/Controllers/EmailController.php
public function sendInvoice(Request $request): JsonResponse
{
    $validated = $request->validate([
        'email' => 'required|email',
        'invoiceId' => 'required|string',
        'amount' => 'required|numeric',
    ]);

    $result = $this->veilmail->client()->emails->send([
        'from' => 'billing@yourdomain.com',
        'to' => $validated['email'],
        'subject' => "Invoice #{$validated['invoiceId']}",
        'templateId' => 'template_xxxxx',
        'templateData' => [
            'invoiceId' => $validated['invoiceId'],
            'amount' => '$' . number_format($validated['amount'], 2),
            'dueDate' => now()->addDays(30)->format('m/d/Y'),
        ],
    ]);

    return response()->json(['emailId' => $result['id']]);
}

5. Handle Webhooks

Receive real-time event notifications. Use $request->getContent() to get the raw body for signature verification.

app/Http/Controllers/WebhookController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class WebhookController extends Controller
{
    public function handle(Request $request): JsonResponse
    {
        $signature = $request->header('X-Veilmail-Signature', '');
        $payload = $request->getContent();
        $secret = config('services.veilmail.webhook_secret');

        $expected = hash_hmac('sha256', $payload, $secret);

        if (!$signature || !hash_equals($expected, $signature)) {
            return response()->json(['error' => 'Invalid signature'], 401);
        }

        $event = json_decode($payload, true);

        match ($event['type']) {
            'email.delivered' => logger()->info("Delivered: {$event['data']['emailId']}"),
            'email.bounced' => logger()->warning("Bounced: {$event['data']['emailId']}"),
            'email.complained' => logger()->warning("Complaint: {$event['data']['emailId']}"),
            'subscriber.unsubscribed' => logger()->info("Unsubscribed: {$event['data']['subscriberId']}"),
            default => null,
        };

        return response()->json(['status' => 'ok']);
    }
}
routes/api.php
// routes/api.php
use App\Http\Controllers\WebhookController;

Route::post('/webhooks/veilmail', [WebhookController::class, 'handle']);

Important: Exclude the webhook route from Laravel's CSRF verification by adding it to the $except array in your VerifyCsrfToken middleware, or use the api route group which skips CSRF by default.

6. Manage Subscribers

Add and manage subscribers from your API:

app/Http/Controllers/SubscriberController.php
// Add a subscriber when a user signs up
public function subscribe(Request $request): JsonResponse
{
    $validated = $request->validate([
        'email' => 'required|email',
        'firstName' => 'required|string',
        'lastName' => 'required|string',
    ]);

    $subscriber = $this->veilmail->client()->audiences
        ->subscribers('audience_xxxxx')
        ->add([
            'email' => $validated['email'],
            'firstName' => $validated['firstName'],
            'lastName' => $validated['lastName'],
            'metadata' => ['source' => 'website'],
        ]);

    return response()->json(['subscriberId' => $subscriber['id']]);
}

// Unsubscribe
public function unsubscribe(Request $request): JsonResponse
{
    $validated = $request->validate([
        'subscriberId' => 'required|string',
    ]);

    $this->veilmail->client()->audiences
        ->subscribers('audience_xxxxx')
        ->remove($validated['subscriberId']);

    return response()->json(['success' => true]);
}

7. Error Handling

The SDK throws typed exceptions you can catch and handle:

app/Http/Controllers/EmailController.php
use VeilMail\VeilMail;
use VeilMail\Exceptions\VeilMailException;
use VeilMail\Exceptions\RateLimitException;

public function send(Request $request): JsonResponse
{
    try {
        $result = $this->veilmail->client()->emails->send([
            'from' => 'hello@yourdomain.com',
            'to' => $request->input('email'),
            'subject' => 'Hello',
            'html' => '<p>Hi!</p>',
        ]);

        return response()->json(['id' => $result['id']]);
    } catch (RateLimitException $e) {
        return response()->json(['error' => 'Rate limited, try again later'], 429);
    } catch (VeilMailException $e) {
        return response()->json(['error' => $e->getMessage()], $e->getStatusCode());
    } catch (\Exception $e) {
        return response()->json(['error' => 'Internal error'], 500);
    }
}