Sending transactional email from Laravel with a real API
Laravel's Mail facade defaults to SMTP, configured through a mail driver in config/mail.php. Switching a transactional send to Envello's HTTP API means either writing a custom Mail transport or, more simply for a focused set of transactional emails, calling the API directly and keeping Laravel's Blade views for rendering.
The direct approach
Render your Blade view to a string, then POST it to Envello's API with Laravel's built-in Http facade, no extra package required:
- $html = view('emails.welcome', ['name' => $user->name])->render();
- Http::withToken(config('services.envello.key'))->post('https://api.envello.dev/emails', ['from' => 'Acme <[email protected]>', 'to' => $user->email, 'subject' => 'Welcome!', 'html' => $html]);
- Check ->successful() on the response and handle a failed send explicitly rather than assuming it worked
Configuration
Store the API key in config/services.php, loaded from .env. This follows Laravel's convention for third-party service credentials.
- In .env: ENVELLO_API_KEY=your_api_key_here and ENVELLO_FROM='YourApp <[email protected]>'
- In config/services.php: 'envello' => ['key' => env('ENVELLO_API_KEY'), 'from' => env('ENVELLO_FROM')]
- Access with config('services.envello.key') and config('services.envello.from')
- Never commit .env to source control. Document new variables in .env.example.
Creating a service class
Wrap the API call in a service class so every call site uses the same configuration and error handling.
- Create app/Services/EnvelloEmailService.php
- Constructor-inject the config values or use config() helper
- Public method: send(string $to, string $subject, string $html, ?string $text = null): SendResult
- Use Http::withToken($this->apiKey)->post($this->baseUrl . '/emails', $payload)
- Return a value object with messageId on success, or throw a typed exception on failure
- Register the service in AppServiceProvider if you want to inject it via dependency injection
Error handling
Laravel's Http facade provides clean methods for inspecting responses.
- $response->successful(): 2xx, message accepted. Log the message ID.
- $response->status() === 400: validation error. Don't retry. Log $response->json() for details.
- $response->status() === 429: rate limited. Throw an exception that your queue's retry logic can handle.
- $response->serverError(): 5xx. Retry with backoff.
- $response->throw(): throws RequestException on any non-2xx. Useful for simple cases where you want a single catch block.
- Use Http::retry(3, 100)->withToken(...) to add automatic retries with a 100ms delay for transient failures.
Keeping this out of the request cycle
For anything triggered by a web request, wrap the send in a queued job (Laravel's built-in queue system) rather than calling the API synchronously inside a controller. That keeps a slow or failed API call from holding up the response to your user, and gives you Laravel's existing job-retry mechanism for free if the send fails transiently.
- Create a job: php artisan make:job SendTransactionalEmail
- Accept the payload in the constructor, call the email service in handle()
- Set $tries = 3 and $backoff = [10, 60, 300] for escalating retry delays
- Dispatch from your controller: SendTransactionalEmail::dispatch($to, $subject, $html)
- The controller returns immediately; the queue worker handles delivery
Using Blade templates for email
Laravel's Blade engine renders email HTML the same way it renders web pages. Create templates in resources/views/emails/ and render with view()->render().
For responsive email layouts, use a dedicated email layout in resources/views/emails/layouts/. Email HTML has different constraints than browser HTML (inline CSS, table-based layouts for Outlook), so keep email layouts separate from web layouts.
Laravel's built-in Markdown mail (Mail::markdown()) generates responsive HTML automatically. You can use this with the direct API approach by rendering the Markdown to HTML and passing the result as the html field.
If you want an SDK instead
A PHP SDK exists (github.com/Aktai-ltd/envello-php, composer require envello/envello-php), but it's honest to say it isn't published on Packagist yet, you'd need to require it as a VCS repository pointing at that git URL for now. For most Laravel apps, Http::withToken(...)->post(...) is few enough lines that waiting on a published package isn't a real blocker.
Webhook handling
Set up a route and controller to receive Envello's delivery event webhooks.
- Create a route in routes/api.php: Route::post('/webhooks/envello', [WebhookController::class, 'envello']). API routes skip CSRF verification automatically.
- Read the raw request body: $request->getContent()
- Get the signature: $request->header('X-Envello-Signature')
- Verify HMAC-SHA256: hash_equals(hash_hmac('sha256', $rawBody, $secret), $signature)
- Return response with 401 status if signature is invalid
- Parse the event and handle: delivered (log), bounced (suppress address in your users table or a dedicated table), complained (suppress)
- Return response()->json(['received' => true], 200)
Suppression tracking
Create a table to track suppressed email addresses so your application avoids sending to addresses that will bounce or generate complaints.
- php artisan make:model EmailSuppression -m
- Migration: email (string, unique index), reason (string: bounced/complained), created_at
- Before sending: if (EmailSuppression::where('email', $to)->exists()) return early
- In the webhook handler: EmailSuppression::firstOrCreate(['email' => $eventEmail], ['reason' => $eventType])
Testing
For unit tests, use Http::fake() to mock the API response and verify the request payload. Laravel's HTTP client fake returns a customizable response sequence, so you can test success, validation errors, and server errors in isolation.
For feature tests, fake the HTTP client and test the full flow: controller receives form submission, dispatches job, job calls email service, service makes the mocked API call.
For integration tests with real API calls, use Envello's test mode to validate end-to-end without delivering to real inboxes.
Check your domain's SPF, DKIM, and DMARC records
Paste in a domain and see what's missing, plus the exact DNS records to fix it. Free, no account needed.
Check your domain →