Guides / Laravel transactional email

How to send transactional email from Laravel

Short answer: Register a SendFox mail transport with Mail::extend(), add a sendfox mailer and set MAIL_MAILER=sendfox. Every Mail::to() call, Mailable and notification, including Laravel's password reset emails, then sends through SendFox's API. No package to install. Free for 3,000 emails a month.

Get a free API key

This works on Laravel 10, 11 and 12. It uses the Symfony Mailer transport API Laravel is built on and Laravel's own HTTP client.

Step 1: Get an API key

Sign up free and open Transactional. Your key is created on the first visit.

Step 2: Add the transport

It turns each message into one API call and throws with the API's explanation when something's wrong, so a failed send in your logs says what to fix.

app/Mail/SendFoxTransport.php

<?php

namespace App\Mail;

use Illuminate\Support\Facades\Http;
use RuntimeException;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\AbstractTransport;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\MessageConverter;

class SendFoxTransport extends AbstractTransport
{
    public function __construct(private string $apiKey)
    {
        parent::__construct();
    }

    protected function doSend(SentMessage $message): void
    {
        $email = MessageConverter::toEmail($message->getOriginalMessage());

        if ($email->getAttachments() !== []) {
            throw new RuntimeException('SendFox does not support attachments yet.');
        }

        $from = $email->getFrom()[0];
        $addresses = fn (array $list): array => array_map(fn (Address $address) => $address->getAddress(), $list);

        $response = Http::withToken($this->apiKey)
            ->post('https://api.sendfox.com/transactional/emails', array_filter([
                'from' => $from->getName() ? $from->getName() . ' <' . $from->getAddress() . '>' : $from->getAddress(),
                'to' => $addresses($email->getTo()),
                'cc' => $addresses($email->getCc()),
                'bcc' => $addresses($email->getBcc()),
                'reply_to' => ($email->getReplyTo()[0] ?? null)?->getAddress(),
                'subject' => $email->getSubject(),
                'html' => $email->getHtmlBody(),
                'text' => $email->getTextBody(),
            ]));

        if ($response->failed()) {
            // Every error has a stable name and a fix, e.g. "domain_not_verified".
            throw new RuntimeException($response->json('name') . ': ' . $response->json('fix'));
        }
    }

    public function __toString(): string
    {
        return 'sendfox';
    }
}

Step 3: Register it

app/Providers/AppServiceProvider.php

use App\Mail\SendFoxTransport;
use Illuminate\Support\Facades\Mail;

public function boot(): void
{
    Mail::extend('sendfox', fn () => new SendFoxTransport(config('services.sendfox.key')));
}

Step 4: Configure the mailer

// config/services.php
'sendfox' => [
    'key' => env('SENDFOX_API_KEY'),
],

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

.env

MAIL_MAILER=sendfox
[email protected]
MAIL_FROM_NAME=Acme
SENDFOX_API_KEY=your_sendfox_key

Run php artisan config:clear if your config is cached.

Step 5: Send

Your existing code doesn't change:

use App\Mail\OrderShipped;
use Illuminate\Support\Facades\Mail;

Mail::to($order->user)->send(new OrderShipped($order));

// Or only for some mail, keeping another default mailer:
Mail::mailer('sendfox')->to($user)->send(new OrderShipped($order));

Implement ShouldQueue on a Mailable to send it from a queue worker, so a slow request never waits on email.

Step 6: Test, then verify your domain

Send to [email protected] first. Test addresses never reach a mailbox and never count against your quota, and [email protected] and [email protected] fake those outcomes so you can see your error handling and webhook fire. Until your domain is verified, send from [email protected], which delivers to the test addresses and your own account email.

Then add your domain under Account > Domains, paste the DNS records into your DNS provider and click verify. After that, send from any address on it.

Things to know

Full reference: Transactional API docs, or the same docs as Markdown to hand to your coding agent.

FAQ

Is there a SendFox package for Laravel?

You do not need one. Laravel supports custom mail transports, and the one on this page is about 40 lines using the HTTP client Laravel already ships with.

Do password reset emails and notifications go through it too?

Yes. Anything that uses the default mailer does, including Laravel's password reset and email verification notifications and your own Mailables and notifications.

Can I use SMTP instead?

No. SendFox does not offer SMTP. The transport on this page uses the HTTPS API instead, which is faster and gives you clearer errors.

How much does it cost?

Free for 3,000 emails a month. $15 a month for 50,000.

Send your first email in two minutes.

Free for 3,000 emails a month. $15 a month for 50,000. Already on Resend? Change two environment variables.

Get your free API key

See everything the email API does