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.
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
- Attachments aren't supported yet, so the transport throws rather than sending without them.
- Rate limit: 5 requests a second. A queue with a few workers stays well under it.
- Every recipient counts. Each
to,ccandbccaddress is one email against your quota. - Webhooks for delivered, bounced and complained events are set up under Transactional > Webhooks, signed with an HMAC you verify in your controller.
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