Back to blog
/9 min read

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