Send email with Laravel

A queued job that sends, with the key in config and never in a controller.

You need a verified sending domain and an API key from Settings › API Keys. If neither exists yet, the quickstart covers both in a couple of minutes.

Send

<?php
 
use IlluminateBusQueueable;
use IlluminateContractsQueueShouldQueue;
use IlluminateSupportFacadesHttp;
 
class SendWelcome implements ShouldQueue
{
    use Queueable;
 
    public function __construct(public string $email, public string $userId) {}
 
    public function handle(): void
    {
        Http::withToken(config('services.sending.key'))
            ->post('https://sending.dev/api/v1/emails', [
                'from' => 'Acme <[email protected]>',
                'to' => $this->email,
                'subject' => 'Welcome',
                'html' => '<p>You are in.</p>',
                'idempotencyKey' => "welcome-{$this->userId}",
            ])->throw();
    }
}

A successful call answers 202 with the message id: accepted and queued, not yet delivered. Delivery, opens, bounces and complaints arrive later, on webhooks or in the dashboard.

What changes in Laravel

Http::withToken sets the bearer header, and ->throw() turns a 4xx into an exception the queue can retry. Add the key under config/services.php rather than reading env() at runtime, so config caching keeps working.

The thing that catches people

A failed job is retried by the queue, which is exactly when a stable idempotencyKey earns its place: without it every retry is another email in the customer's inbox.

Next

  • All the fields: cc and bcc, reply-to, attachments, templates, scheduling.
  • Webhooks: delivery, bounce and complaint events, signed with HMAC.
  • Domains: SPF, DKIM and MAIL FROM, and why sending is refused until they are in place.
  • MCP: the same operations as tools, when the one writing the code is an agent.

Nearby: PHP and Ruby

All 30 stacks