How to Send Transactional Email from Laravel with ZidiMail
A practical guide to sending transactional email from a Laravel application using the ZidiMail REST API. Covers HTTP Client, Mailables, queuing, and testing.
Laravel gives you several ways to send email: the Mail facade, raw HTTP calls, or a custom mail transport. This guide covers all three approaches, starting with the simplest — a direct HTTP call using Laravel's built-in HTTP Client — and building up to a fully queued Mailable that plugs into the Mail facade.
Prerequisites
- A ZidiMail account (free, no credit card)
- A verified sending domain
- An API key from the ZidiMail dashboard
- Laravel 10 or 11
1. Store your credentials
Add to your .env file:
ZIDIMAIL_API_KEY=zm_live_your_full_key
ZIDIMAIL_FROM_ADDRESS=hello@mail.yourdomain.com
ZIDIMAIL_FROM_NAME="Your App Name"Then expose them in config/services.php:
// config/services.php
'zidimail' => [
'key' => env('ZIDIMAIL_API_KEY'),
'from_address' => env('ZIDIMAIL_FROM_ADDRESS'),
'from_name' => env('ZIDIMAIL_FROM_NAME', 'ZidiMail'),
],2. Create a reusable service class
<?php
// app/Services/ZidiMailService.php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\RequestException;
class ZidiMailService
{
private string $baseUrl = 'https://api.zidimails.com/v1';
private string $apiKey;
private string $fromAddress;
private string $fromName;
public function __construct()
{
$this->apiKey = config('services.zidimail.key');
$this->fromAddress = config('services.zidimail.from_address');
$this->fromName = config('services.zidimail.from_name');
}
public function send(
string|array $to,
string $subject,
string $html,
string $text = '',
string $replyTo = '',
array $headers = []
): array {
$payload = [
'from' => "{$this->fromName} <{$this->fromAddress}>",
'to' => is_array($to) ? $to : [$to],
'subject' => $subject,
'html' => $html,
];
if ($text) $payload['text'] = $text;
if ($replyTo) $payload['reply_to'] = $replyTo;
if ($headers) $payload['headers'] = $headers;
return Http::withToken($this->apiKey)
->timeout(15)
->retry(2, 500)
->post("{$this->baseUrl}/emails", $payload)
->throw()
->json();
}
}3. Send a welcome email on registration
<?php
// app/Listeners/SendWelcomeEmail.php
namespace App\Listeners;
use App\Events\UserRegistered;
use App\Services\ZidiMailService;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendWelcomeEmail implements ShouldQueue
{
public function __construct(private ZidiMailService $mailer) {}
public function handle(UserRegistered $event): void
{
$user = $event->user;
$this->mailer->send(
to: $user->email,
subject: "Welcome to Acme, {$user->name}!",
html: view('emails.welcome', ['user' => $user])->render(),
text: "Welcome {$user->name}. Visit https://app.yourdomain.com to get started.",
replyTo: 'support@yourdomain.com',
);
}
}4. Blade email template
{{-- resources/views/emails/welcome.blade.php --}}
<!DOCTYPE html>
<html lang="en">
<body style="font-family: sans-serif; color: #1a1a1a; max-width: 560px; margin: 0 auto; padding: 32px 16px;">
<h1 style="font-size: 24px; font-weight: 700;">Welcome, {{ $user->name }}!</h1>
<p>Your account is ready. Click below to get started.</p>
<a href="{{ config('app.url') }}"
style="display:inline-block;background:#2563eb;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:600;">
Open your dashboard
</a>
<p style="color:#6b7280;font-size:13px;margin-top:32px;">
If you did not create this account, you can safely ignore this email.
</p>
</body>
</html>5. Queue email jobs for reliability
For production, always queue email sends. Use Laravel's built-in queue with a simple job:
<?php
// app/Jobs/SendTransactionalEmail.php
namespace App\Jobs;
use App\Services\ZidiMailService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SendTransactionalEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $backoff = 30; // seconds between retries
public function __construct(
private string $to,
private string $subject,
private string $html,
private string $text = '',
) {}
public function handle(ZidiMailService $mailer): void
{
$mailer->send($this->to, $this->subject, $this->html, $this->text);
}
}
// Dispatch from anywhere:
// SendTransactionalEmail::dispatch($user->email, 'Order confirmed', $html, $text);6. Testing
In tests, swap the service with a mock so no real HTTP calls are made:
<?php
// tests/Feature/RegistrationTest.php
use App\Services\ZidiMailService;
use Mockery;
public function test_welcome_email_sent_on_registration(): void
{
$mock = Mockery::mock(ZidiMailService::class);
$mock->shouldReceive('send')
->once()
->with(Mockery::type('string'), Mockery::containsString('Welcome'), Mockery::any(), Mockery::any());
$this->app->instance(ZidiMailService::class, $mock);
$this->post('/register', [
'name' => 'Jane Doe',
'email' => 'jane@example.com',
'password' => 'secret123',
])->assertRedirect('/dashboard');
}Common issues
- "422 Unprocessable" — your from address domain must be verified in ZidiMail. Check the Domains tab.
- Http::retry() throwing after all attempts — catch RequestException and log it; do not let an email failure crash your request.
- Long response times — set a timeout (15 seconds is safe). Queue sends in production to avoid blocking HTTP responses.
- Duplicate sends on queue retry — pass "X-Idempotency-Key" in headers using a stable ID (e.g. order ID) to prevent re-delivery.
Start sending from Laravel in minutes
Free ZidiMail account, verified domain, first email sent — all in under 30 minutes.
Start sending free