INTEGRATIONS · Published 2026-10-06 · Updated 2026-10-06 · SecureSMTP

Laravel

Use Laravel’s SMTP mailer with SecureSMTP, or call the API with the HTTP client.

Two ways: point Laravel’s SMTP mailer at SecureSMTP, so Mail:: and every Mailable and notification use it with no code changes, or call the API with the HTTP client.

SMTP mailer

Set these in .env. The password is your site’s API key — see Sites and API keys.

.env
MAIL_MAILER=smtp
MAIL_HOST=smtp.securessmtp.com
MAIL_PORT=587
MAIL_USERNAME=securessmtp
MAIL_PASSWORD=qcs_live_...
MAIL_FROM_ADDRESS="[email protected]"
MAIL_FROM_NAME="${APP_NAME}"

Port 587 uses STARTTLS automatically. For port 465 set MAIL_PORT=465 and, on Laravel 11 and later, MAIL_SCHEME=smtps. If you cache your config, run php artisan config:clear after changing .env.

PHP
use Illuminate\Support\Facades\Mail;

// Any Mailable works the same way:
Mail::to('[email protected]')->send(new OrderShipped($order));

// Or a plain-text message:
Mail::raw('Your order is on its way.', function ($message) {
    $message->to('[email protected]')->subject('Your order has shipped');
});
In relay mode (no verified domain) the From address is replaced: the email goes out from [email protected] with your From name, and the From address you set becomes the Reply-To. Verify your domain and the email goes out from your own address. See Domains and DNS.

To send in the background, use Mail::to(...)->queue(...) or make the Mailable implement ShouldQueue. A refused email throws a Symfony Mailer TransportException whose message includes the SMTP reply code:

SMTP replyMeaning
250Accepted.
535Login failed: the password is not a valid API key.
450 4.7.0Rate limit: more than 120 sends from this site in 60 seconds. Retry later.
550 5.7.0Monthly email limit reached.
550 5.7.1Rejected as spam, the site is disabled, or every recipient has unsubscribed.
550 5.1.1Every recipient is on the suppression list.
552 5.3.4Attachments are over 15 MB in total.
4xx 4.3.0Temporary failure. Retry later.

HTTP client

Add the key to .env as SECURESSMTP_API_KEY=qcs_live_... and read it through a config file, so it still works after php artisan config:cache:

config/services.php
// config/services.php
'securessmtp' => [
    'key' => env('SECURESSMTP_API_KEY'),
],
PHP
use Illuminate\Support\Facades\Http;

$response = Http::withHeaders([
        'x-securessmtp-api-key' => config('services.securessmtp.key'),
    ])
    ->timeout(30)
    ->post('https://securessmtp.com/api/v1/mail/send', [
        'to' => '[email protected]',
        'subject' => 'Your order has shipped',
        'html' => '<p>Your order is on its way.</p>',
        'from' => ['name' => config('app.name')],
    ]);

if (! $response->json('ok')) {
    $reason = $response->json('reason') ?? $response->json('error') ?? 'HTTP '.$response->status();
    throw new RuntimeException("SecureSMTP: {$reason}");
}

$messageId = $response->json('message_id');

The HTTP client sends the array as JSON. from must be an array with a name.

Queued job

Sending from a job keeps slow network calls out of web requests. This job retries temporary failures and stops at once on errors that a retry will not fix:

app/Jobs/SendEmailViaSecureSMTP.php
<?php
// app/Jobs/SendEmailViaSecureSMTP.php (Laravel 11 or later)

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Http;
use RuntimeException;

class SendEmailViaSecureSMTP implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;

    public int $backoff = 60;

    public function __construct(public array $message)
    {
    }

    public function handle(): void
    {
        $response = Http::withHeaders([
                'x-securessmtp-api-key' => config('services.securessmtp.key'),
            ])
            ->timeout(30)
            ->post('https://securessmtp.com/api/v1/mail/send', $this->message);

        if ($response->json('ok')) {
            return;
        }

        $reason = $response->json('reason') ?? $response->json('error') ?? 'HTTP '.$response->status();

        if ($reason === 'rate_limited' || $response->serverError()) {
            // Temporary: try again later.
            throw new RuntimeException("SecureSMTP: {$reason}");
        }

        // 400/401/403, over_quota, send_failed: retrying will not help.
        $this->fail(new RuntimeException("SecureSMTP: {$reason}"));
    }
}
PHP
use App\Jobs\SendEmailViaSecureSMTP;

SendEmailViaSecureSMTP::dispatch([
    'to' => $user->email,
    'subject' => 'Welcome',
    'text' => 'Thanks for signing up.',
    'from' => ['name' => config('app.name')],
]);
There is no idempotency key. If a request times out after SecureSMTP accepted it, a retry sends the email a second time. Keep $tries low.

Handle errors

ResponseMeaningWhat to do
200 ok: trueAccepted for sending.Nothing. Keep message_id if you want to find the email later.
200 rate_limitedMore than 120 sends from this site in 60 seconds.Wait a minute, then try again.
200 over_quotaThe account’s monthly email count is used up (relay mode only).Wait for the new month, upgrade, or send from a verified domain.
200 send_failedThe email was not sent. The error field says why.Read error. Do not retry in a loop.
400 invalid_payload, no_body, …The request is wrong. details lists the fields for invalid_payload.Fix the request. Retrying the same request fails the same way.
400 content_flagged, ai_flaggedThe content was judged to be spam.Change the content. See Deliverability.
401 missing_api_key, invalid_api_keyNo key was sent, or the key is wrong or was rotated.Check the environment variable on the server.
403 site_disabledThe site is disabled.See Blocks.
  • Check ok in the body, not only the HTTP status. Rate limit, monthly limit and send failures come back with HTTP 200.
  • There is no idempotency key. If a request times out, the email may already have been sent, and sending it again can deliver it twice.
  • Every reason is listed in the error catalogue.

Next