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.