Framework guides
Send email from Laravel
Send transactional email from Laravel with Http::, as a service class and as a queued job.
This is the same POST /api/v1/emails endpoint documented in
Send a transactional email, wired into
Laravel with the built-in Http facade — no extra package needed.
Service class#
<?php
// app/Services/BytloopMailClient.php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class BytloopMailClient
{
public static function sendEmail(
string $from,
array|string $to,
string $subject,
string $html,
string $idempotencyKey,
array $tags = [],
): array {
$response = Http::withToken(config('services.bytloop_mail.api_key'))
->withHeaders(['Idempotency-Key' => $idempotencyKey])
->post('https://mail.bytloop.com/api/v1/emails', [
'from' => $from,
'to' => is_array($to) ? $to : [$to],
'subject' => $subject,
'html' => $html,
'tags' => $tags,
]);
if ($response->failed()) {
throw new RuntimeException(
"Bytloop Mail send failed ({$response->status()}): {$response->body()}"
);
}
return $response->json();
}
}// config/services.php
'bytloop_mail' => [
'api_key' => env('BYTLOOP_API_KEY'),
],Call it directly for a one-off send:
use App\Services\BytloopMailClient;
BytloopMailClient::sendEmail(
from: 'Support <support@send.acme.com>',
to: $order->customer_email,
subject: "Your receipt for order #{$order->id}",
html: view('emails.receipt', ['order' => $order])->render(),
idempotencyKey: "receipt-{$order->id}",
tags: ['receipt_for_order' => (string) $order->id],
);Queued job (recommended)#
An Http::post() call inside a controller blocks the response on the
API round-trip. Wrap it in a queued job — the same pattern Laravel's own
Mail::send() uses with ShouldQueue — so the response comes back
immediately and a transient failure retries instead of failing silently:
<?php
// app/Jobs/SendReceiptEmail.php
namespace App\Jobs;
use App\Models\Order;
use App\Services\BytloopMailClient;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SendReceiptEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 5;
public array $backoff = [10, 30, 60, 120, 300];
public function __construct(public Order $order) {}
public function handle(): void
{
BytloopMailClient::sendEmail(
from: 'Support <support@send.acme.com>',
to: $this->order->customer_email,
subject: "Your receipt for order #{$this->order->id}",
html: view('emails.receipt', ['order' => $this->order])->render(),
idempotencyKey: "receipt-{$this->order->id}",
tags: ['receipt_for_order' => (string) $this->order->id],
);
}
}// wherever the order is confirmed
SendReceiptEmail::dispatch($order);Common pitfalls#
- Calling
BytloopMailClient::sendEmailsynchronously from a controller. Blocks the request on the API round-trip for no reason the user can see. Use the queued job above for anything triggered by a request. - No
$backoff/$tries. Without it, aQUEUE_CONNECTION=synclocal setup masks the problem, but in production a transient network error or a5xxduring deploy just fails the job once and stops. - Passing the API key directly with
env()outsideconfig/. Laravel caches config in production (config:cache); readingenv()directly in application code silently returnsnullonce that cache is warm. Always go throughconfig('services.bytloop_mail.api_key')as shown above.
Next#
- Register a webhook endpoint (a Laravel route) to hear about delivery, bounces, and opens.
- Read the rate limits page so a bulk send (e.g. an
Artisan command iterating
Order::where(...)->cursor()) doesn't get throttled mid-run.