Laravel
Send transactional emails from your Laravel application using the Veil Mail PHP SDK.
Prerequisites
- Laravel 10 or later
- PHP 8.1+
- A Veil Mail API key
- A verified domain
1. Install the SDK
composer require veilmail/veilmail-php2. 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_xxxxxapp/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);
}
}