Every account now comes with a free mailbox — warmed automatically, no domain to buy. See how it works
Laravel's Mail facade sends over SMTP or a provider's driver, and Warmerly's transactional API is REST only, so the Mail facade cannot use it today. The Http facade can: a small service class makes the request, and you call it from a controller, a notification or a queued job.
Four steps, the same in every framework. Warmerly is REST only today: no SMTP relay, webhooks, templates or attachments yet.
In Warmerly open Transactional email, add the domain you send from (for example mail.example.com), publish the DNS records it shows at your DNS provider, then verify. Use a domain or subdomain you do not use for cold outreach.
Under Transactional email, API keys, create a key with the Sending permission, optionally limited to that domain. It starts with wm_tx_ and is shown once, so copy it straight into your secrets store.
Add WARMERLY_TX_KEY to your .env file and read it through config/services.php, so config caching keeps working.
A successful request answers 202 with the email's id and a status of sent (or queued, which Warmerly retries for up to 24 hours). The free plan is 5,000 emails a month with no card.
Copy these into your project. The request is the same one the API reference documents; nothing is hidden behind a package.
<?php
// app/Services/Warmerly.php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class Warmerly
{
private const ENDPOINT = 'https://app.warmerly.com/api/v1/transactional/emails';
/**
* Send one email. $idempotencyKey identifies the MESSAGE (for example "reset-{$user->id}-{$tokenId}"),
* so retrying the same message never sends it twice.
*
* @param string|array<int, string> $to
* @return array{id: string, status: string, duplicate: bool}
*/
public static function send(
string $from,
string|array $to,
string $subject,
?string $html,
?string $text,
string $idempotencyKey,
): array {
$response = Http::withToken(config('services.warmerly.key'))
->withHeaders(['Idempotency-Key' => $idempotencyKey])
->timeout(15)
->post(self::ENDPOINT, array_filter([
'from' => $from,
'to' => $to,
'subject' => $subject,
'html' => $html,
'text' => $text,
], fn ($value) => $value !== null));
if ($response->status() !== 202) {
throw new RuntimeException(sprintf('Warmerly %d %s', $response->status(), $response->json('error.code', 'unknown')));
}
return $response->json(); // ['id' => ..., 'status' => 'sent'|'queued', 'duplicate' => false]
}
}
// config/services.php
'warmerly' => [
'key' => env('WARMERLY_TX_KEY'),
],
use App\Services\Warmerly;
Warmerly::send(
from: 'Acme <noreply@mail.example.com>',
to: $user->email,
subject: 'Reset your password',
html: '<p>Click <a href="'.e($url).'">here</a> to reset your password.</p>',
text: "Reset your password: {$url}",
idempotencyKey: "reset-{$user->id}-{$tokenId}",
);
Every refusal has a code in the response body, at error.code. These are the ones to handle, and whether to retry.
| Status | Code | Meaning | Retry? |
|---|---|---|---|
| 400 | bad_request | Invalid body, or the From address is not on a verified sending domain. | No: fix the request |
| 403 | forbidden | The key is limited to a different sending domain. | No |
| 403 | sending_paused | Sending is paused for the workspace (bounce or complaint rate, or under review). | No: contact support |
| 422 | content_rejected | The message was refused by abuse screening. | No, not unchanged |
| 429 | rate_limited | The hourly or daily sending limit was reached. | Later |
| 429 | quota_exceeded | The plan's monthly allowance, plus the free buffer, is used up. | No: upgrade or wait |
| 502 | send_failed | It could not be sent or queued. details.interrupted says whether it may have been delivered. | Same key if interrupted is false |
| 503 | service_unavailable | Transactional sending is not enabled for the workspace yet. | No |
Full details, including the sending limits, are in the transactional email API reference.
Not today. The Mail facade needs an SMTP server or a supported API driver, and Warmerly's transactional API is REST only. Call the API with the Http facade, as above.
Yes. It is a plain HTTP request with a timeout, with no state, so it works anywhere the Http facade does. Use a queued job for anything you do not want in the request cycle.
No. Warmerly's transactional API is one HTTP request, so there is nothing to install. The code on this page is the whole integration.
Not yet. The send request takes from, to, cc, bcc, reply_to, subject, html or text, headers and tags. Attachments, SMTP relay, delivery webhooks and templates are not available today.
If nothing was handed over, the email is queued rather than dropped. The request still answers 202 with a status of queued, and Warmerly retries it after 1, 5, 15 and 60 minutes, then hourly, for up to 24 hours.
Create a free account, verify a sending domain and send a test. 5,000 emails a month free, no card.