sendsend.email

Laravel mail driver

Make Mail::send(), notifications and queued mail in Laravel go through SendSend, with one small class and no package to install.

Laravel sends mail through a “transport”. Add a SendSend transport once and everything that already sends mail in your app uses it: Mail::to(), mailables, notifications, password resets and queued mail. For a one-off send from a controller instead, see Send email from Laravel. This guide needs Laravel 9 or newer.

1. Add your key and From address

Create a live API key in the dashboard with the emails:send scope, and use a From address on a domain you have verified. In .env:

MAIL_MAILER=sendsend
SENDSEND_API_KEY=ss_live_...
MAIL_FROM_ADDRESS="hello@yourdomain.com"
MAIL_FROM_NAME="Your App"

2. Add the mailer

In config/mail.php, inside 'mailers':

'sendsend' => [
    'transport' => 'sendsend',
    'key' => env('SENDSEND_API_KEY'),
],

3. Add the transport

<?php

namespace AppMail;

use IlluminateSupportFacadesHttp;
use SymfonyComponentMailerExceptionTransportException;
use SymfonyComponentMailerSentMessage;
use SymfonyComponentMailerTransportAbstractTransport;
use SymfonyComponentMimeAddress;
use SymfonyComponentMimeMessageConverter;

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

    protected function doSend(SentMessage $message): void
    {
        $email = MessageConverter::toEmail($message->getOriginalMessage());
        $addresses = fn (array $list) => array_values(array_map(fn (Address $a) => $a->getAddress(), $list));
        $from = $email->getFrom()[0];
        $name = preg_replace('/["<>\r\n]/', '', $from->getName());

        $payload = array_filter([
            'from' => $name ? "{$name} <{$from->getAddress()}>" : $from->getAddress(),
            'to' => $addresses($email->getTo()),
            'cc' => $addresses($email->getCc()) ?: null,
            'bcc' => $addresses($email->getBcc()) ?: null,
            'reply_to' => $addresses($email->getReplyTo()) ?: null,
            'subject' => $email->getSubject(),
            'html' => $email->getHtmlBody(),
            'text' => $email->getTextBody(),
        ], fn ($value) => $value !== null && $value !== '');

        foreach ($email->getAttachments() as $part) {
            $headers = $part->getPreparedHeaders();
            $payload['attachments'][] = array_filter([
                'filename' => $headers->getHeaderParameter('Content-Disposition', 'filename') ?? 'attachment',
                'content' => base64_encode($part->getBody()),
                'content_type' => $part->getMediaType() . '/' . $part->getMediaSubtype(),
            ]);
        }

        $response = Http::withToken($this->key)
            ->withHeaders(['Idempotency-Key' => sha1($message->getMessageId())]) // a retry can never send twice
            ->acceptJson()
            ->post('https://api.sendsend.email/v1/emails', $payload);

        if ($response->failed()) {
            throw new TransportException('SendSend: ' . ($response->json('error.message') ?? $response->body()));
        }
    }

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

4. Register it

In app/Providers/AppServiceProvider.php:

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

public function boot(): void
{
    Mail::extend('sendsend', fn (array $config) => new SendSendTransport($config['key']));
}

5. Try it

Mail::raw('It works.', fn ($m) => $m->to('you@example.com')->subject('Test from Laravel'));

Open the message in your dashboard to see it move from queued to delivered. After that, nothing else in your app changes:

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

$user->notify(new InvoicePaid($invoice));

Mail::to($user)->queue(new ReceiptMail($order)); // queued mail is retried safely

Good to know

  • Attachments are sent too, up to 10 files and 10 MB in total.
  • Each message carries an idempotency key made from its Laravel message id, so a queue retry can never deliver the same email twice.
  • For delivery, bounce and complaint events in your app, add a webhook and verify its signature.
  • Test your setup safely with a ss_test_ key first. It simulates delivery and never sends.

Troubleshooting

MessageCause
domain_not_verifiedThe From address’s domain is not verified in the project that owns the key.
account_not_approvedLive sending is waiting for account approval. A test key works meanwhile but never delivers.
unauthorizedThe key is wrong or revoked. Clear cached config with php artisan config:clear after changing .env.
Mailer [sendsend] is not definedThe mailers entry in config/mail.php is missing, or the config is cached.
Unsupported mail transportMail::extend() was not reached. Check that it is in AppServiceProvider::boot().