Laravel integration: send transactional email
Updated
An official package is available: inboxili/inboxili-laravel on Packagist, with a facade, a queued job and a mail transport. The sections below show the same integration done by hand with Laravel's HTTP client.
The official package covers most cases. The rest of this page shows the same integration built by hand with Laravel's Http facade: a small service class, a config entry and a queued job.
Use the official package
composer require inboxili/inboxili-laravel
Add INBOXILI_API_KEY to .env. The package is auto-discovered and gives you:
use Inboxili\Laravel\Facades\Inboxili;
use Inboxili\Laravel\Jobs\SendInboxiliEmail;
Inboxili::emails()->send([
'to' => $user->email,
'from_email' => 'hello@yourdomain.com',
'subject' => 'Welcome, {{first_name}}',
'html_body' => '<p>Hi {{first_name}}, your account is ready.</p>',
'template_data' => ['first_name' => $user->first_name],
]);
SendInboxiliEmail::dispatch([/* same fields */]); // runs once on your queue
It also registers an inboxili mail transport. Set a mailer with 'transport' => 'inboxili' in config/mail.php and MAIL_MAILER=inboxili, and Mailables and notifications go through the API. Because the API takes one recipient and has no CC, BCC, reply-to or attachment fields, the transport throws on a message that uses them instead of dropping them silently. Source: github.com/inboxili/inboxili-laravel. Package: inboxili/inboxili-laravel on Packagist.
Or call the API by hand
Configure
config/services.php:
'inboxili' => [
'key' => env('INBOXILI_API_KEY'),
'base_url' => env('INBOXILI_BASE_URL', 'https://api.inboxili.com/api/v1'),
'from_email' => env('INBOXILI_FROM_EMAIL', 'hello@yourdomain.com'),
'from_name' => env('INBOXILI_FROM_NAME', config('app.name')),
],
.env:
INBOXILI_API_KEY=ik_live_...
INBOXILI_FROM_EMAIL=hello@yourdomain.com
The service
app/Services/Inboxili.php:
<?php
namespace App\Services;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
class Inboxili
{
/**
* @param array{to:string,subject?:string,html_body?:string,text_body?:string,template_id?:string,template_data?:array,from_email?:string,from_name?:string} $message
* @return array{status:string,message_id:?string}
*
* @throws RequestException
*/
public function send(array $message): array
{
$payload = array_filter($message + [
'from_email' => config('services.inboxili.from_email'),
'from_name' => config('services.inboxili.from_name'),
], fn ($v) => $v !== null);
return Http::withToken(config('services.inboxili.key'))
->baseUrl(config('services.inboxili.base_url'))
->timeout(10)
->acceptJson()
->post('/transactional/send', $payload)
->throw()
->json();
}
}
throw() raises RequestException for any 4xx or 5xx. The response body is available as $e->response->json('error.code').
Send from a queued job
<?php
namespace App\Jobs;
use App\Services\Inboxili;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Http\Client\RequestException;
use Illuminate\Queue\InteractsWithQueue;
class SendWelcomeEmail implements ShouldQueue
{
use InteractsWithQueue, Queueable;
public $tries = 4;
public function __construct(public int $userId) {}
public function backoff(): array
{
return [5, 30, 120];
}
public function handle(Inboxili $inboxili): void
{
$user = \App\Models\User::findOrFail($this->userId);
try {
$inboxili->send([
'to' => $user->email,
'subject' => 'Welcome, {{first_name}}',
'html_body' => '<p>Hi {{first_name}}, your account is ready.</p>',
'template_data' => ['first_name' => $user->first_name],
]);
} catch (RequestException $e) {
// Retry rate limits only; everything else will fail again the same way.
if ($e->response->status() !== 429) {
$this->fail($e);
} else {
throw $e;
}
}
}
}
Dispatch with SendWelcomeEmail::dispatch($user->id);.
One caution: a connection timeout inside handle() also throws, and the retry could send the email twice because the API has no idempotency key. For mail that must not duplicate, record a sent_at before the job finishes and check it at the top of handle().
Testing
<?php
use App\Services\Inboxili;
use Illuminate\Support\Facades\Http;
it('sends through the API', function () {
Http::fake([
'api.inboxili.com/*' => Http::response(['status' => 'sent', 'message_id' => 'm1'], 200),
]);
$result = app(Inboxili::class)->send([
'to' => 'ada@example.com',
'subject' => 'Hi',
'html_body' => '<p>Hi</p>',
]);
expect($result['message_id'])->toBe('m1');
Http::assertSent(fn ($r) => $r->hasHeader('Authorization') && $r['to'] === 'ada@example.com');
});
Production notes
- Keep the key in
.envand runphp artisan config:cachein production. - Run
queue:workunder a supervisor so retries happen. - Webhooks: register a route, exclude it from CSRF, and verify with
hash_equals(hash_hmac('sha256', $request->getContent(), $secret), $request->header('X-Inboxili-Signature')). - Laravel's built-in notifications (password reset, email verification) go through
MAIL_MAILER. To send them with Inboxili, define a custom notification channel that calls this service.
Frequently asked questions
- Can I set MAIL_MAILER to Inboxili?
- Yes, with the official package: define a mailer with transport inboxili. The API has one recipient and no CC, BCC, reply-to or attachments, so the transport throws on messages that use them rather than dropping them.
- How do I send from a queue?
- Wrap the service call in a queued job. That also lets Laravel's retry and backoff handle HTTP 429.
Build with Inboxili
Create a workspace, verify a domain, and make your first API call.