sendinblue/api-bundle
A PHP bundle that simplifies integrating Sendinblue’s API, giving applications a convenient way to configure credentials and access transactional email, SMS, and contact features with less boilerplate. Use it to centralize API setup and keep Sendinblue calls organized.
Read this first: sendinblue/api-bundle is a Symfony bundle, not a Laravel package. It ships a Bundle class, DependencyInjection/Configuration.php, and a Resources/config/services.xml — there is no ServiceProvider, no facade, no publishable config, and no config/app.php entry. Installing it in a Laravel app pulls symfony/framework-bundle and friends and gives you nothing you can boot. On Laravel 9+/10/11 (Symfony 6/7 components) it will typically fail dependency resolution outright.
So the day-to-day answer for a Laravel developer is: use the underlying SDK directly and treat the bundle as a reference implementation. The bundle is ~40 lines of XML service definitions; you can reproduce all of it in one service provider, and you keep control of lifetimes, config, and the Brevo rename.
Minimal setup:
composer require sendinblue/api-v3-sdk # or getbrevo/brevo-php
// config/services.php
'sendinblue' => [
'api_key' => env('SENDINBLUE_API_KEY'),
'timeout' => (int) env('SENDINBLUE_TIMEOUT', 10),
],
// app/Providers/SendinblueServiceProvider.php
public function register(): void
{
$this->app->singleton(Configuration::class, fn () => (new Configuration())
->setApiKey('api-key', config('services.sendinblue.api_key'))
->setApiKey('partner-key', config('services.sendinblue.api_key')));
$this->app->singleton(TransactionalEmailsApi::class, fn ($app) =>
new TransactionalEmailsApi(new Client(null, $app->make(Configuration::class)))
);
$this->app->bind(TransactionalMailer::class, SendinblueMailer::class);
}
Where to look first:
vendor/sendinblue/api-v3-sdk/docs/ — every API class, model, and request shape lives here. The bundle never documented any of it, and neither will you get docs from it.vendor/sendinblue/api-bundle/Resources/config/services.xml — if you insist on mirroring the bundle's wiring, this is the only authoritative list of what it would have registered.Configuration.php in the SDK (setApiKey, setHost, setUserAgent) — the only config surface you actually care about.First use case — transactional email: a SendinblueMailer service bound to a TransactionalMailer interface, building a SendSmtpEmail model, calling sendTransacEmail(), returning/logging the messageId. Everything else (contacts sync, campaigns, SMS) is the same shape with a different API class.
Bind interfaces, not SDK classes. Declare App\Mail\Contracts\TransactionalMailer with domain methods (sendWelcome(User $u), sendPasswordReset(string $email, string $token)). Bind the SDK-backed implementation in the provider and typehint the interface everywhere. This is the pattern that survives the Sendinblue→Brevo rename, an SDK major bump, or a swap to Postmark — nothing above the binding changes.
Singleton the Configuration and Client, resolve APIs per use. Registering one Configuration with the API key set once means the key is read at container-build time from config/services.php, so php artisan config:cache works correctly. Never call env() from a service class — after config:cache it returns null.
Consider skipping the SDK entirely. For a Laravel app, the lowest-friction path is often the REST API via the Http facade:
Http::withHeaders(['api-key' => config('services.sendinblue.api_key')])
->retry(3, 200, throw: false)
->timeout(10)
->post('https://api.brevo.com/v3/smtp/email', $payload)
->throw();
You get Laravel's retry/backoff, timeout, Http::fake() in tests, and Http::pool() for batching — all the things the SDK and bundle lack. The tradeoff is losing the typed SendSmtpEmail model validation. For a handful of endpoints, this is usually the right call.
Queue everything that isn't user-blocking. A SendTransactionalEmail job with $tries = 5, $backoff = [10, 30, 60, 120], and retryUntil() gives you retries the SDK has none of. Combine with a Redis throttle for rate limits:
Redis::throttle('brevo')->allow(10)->every(1)->then(
fn () => $this->send(),
fn () => $this->release(10),
);
This is the single biggest operational win over the Symfony bundle's usage model — Laravel's queue is the retry and backpressure layer the package is missing.
Custom Notification channel. If you're already using Laravel notifications, a BrevoChannel (send($notifiable, Notification $notification)) wrapping your TransactionalMailer keeps email sends on the same rails as your other channels and makes them faked/swappable in tests via Notification::fake().
Artisan commands for ops. Contact list sync, campaign status, account credits — php artisan sendinblue:account injecting the relevant API class. Same idea as the bundle's console commands, native to Laravel, schedulable in routes/console.php / Kernel::schedule().
Map to models, don't pass arrays. Build SendSmtpEmail, CreateContact, CreateDoiContact from your Eloquent models in a small mapper class. The SDK models validate on construction and give your IDE something to work with; raw arrays silently produce 400s.
Fake it in tests. Bind a fake TransactionalMailer in TestCase::setUp(), or $this->mock(TransactionalEmailsApi::class). Never let a test hit the real API — there is no sandbox mode, so a leaked key in CI sends real mail to real contacts.
2017-09-12), so a jump to 1.0.0 with empty release notes is unverified change, not a stability signal. Pin an exact version in composer.json, diff vendor/sendinblue/api-bundle after update, and do not assume it's maintained going forward. For a Laravel app this matters less than it sounds, because you shouldn't be installing it anyway.config:cache will silently blank your key if you read env('SENDINBLUE_API_KEY') inside a service, provider closure that runs late, or job. Keep env() exclusively in config/*.php.ApiException on 429/5xx. This is the top production failure mode. Centralize retry in the wrapper or in the queued job; never scatter it at call sites.Sendinblue\Client\ApiException, read getResponseBody() and getCode(), rethrow your own MailDeliveryException so callers aren't coupled to the package. Log the body — Brevo 400s are specific and useful.php artisan sendinblue:health calling getAccount() and run it in your deploy/healthcheck.messageId on every accepted send, with your own correlation ID, via Log::withContext() or an explicit context array. Without it, "did this email go out?" is a manual search in the Brevo dashboard.sendinblue/api-v3-sdk was superseded by getbrevo/brevo-php, with namespace and some host/endpoint changes. Pick one, pin it, and don't have both in the lock file.Configuration for the process lifetime. That's fine for a static API key, but if you ever rotate keys per tenant, resolve the API class per job instead of injecting the singleton into the job's constructor.Configuration object in your register(). There is no vendor override to maintain — a real advantage of not using the bundle.TransactionalMailer interface and a single SDK-backed implementation, migrating to Http-based calls, Brevo's new SDK, or another provider is a change to one class plus one binding.How can I help you explore Laravel packages today?