jeffersongoncalves/laravel-brevo
A Laravel package for integrating Brevo (formerly Sendinblue) into your app, making it easy to send transactional email and work with Brevo’s API from a familiar Laravel setup.
This is a fresh 1.0.0, so treat it as a thin, unproven wrapper: the value is that it saves you hand-rolling Guzzle calls and config plumbing, not that it hides the Brevo API from you.
Install and configure
composer require the package, then check whether it auto-discovers a service provider (Laravel 5.5+ convention). If not, register the provider and any facade alias manually in config/app.php.php artisan vendor:publish --tag=brevo-config (tag name may differ — run php artisan vendor:publish and pick the provider). This is the first place to look: the config file enumerates which features are wired up (mail transport? SMS? webhooks?) and what env keys it expects..env. Expect something like BREVO_API_KEY= — an API v3 key from Brevo → SMTP & API → API Keys. Note the key is scoped; a v3 key is not the same as the SMTP password or a v2 key.php artisan config:clear if you had a cached config before adding the env var. This trips up more people than anything else on day one.Where to look first
@method annotations or interface is the de facto API listing. In a 1.0.0 with sparse docs, php -r a ReflectionClass dump or just lean on IDE autocompletion rather than guessing method names.README for the exception class(es) thrown on non-2xx responses and how to get the raw response body. You will need that within the first hour — Brevo's 400s carry a useful code/message payload.First use case: transactional email
The highest-value, lowest-risk entry point. Either:
MAIL_MAILER=brevo and keep using Mail::to($user)->send(new WelcomeEmail()). Your existing Mailables, Blade views and Mail::fake() tests keep working. Start here if it exists.htmlContent/subject. Use this when you need Brevo-side templates that marketing can edit without a deploy.Resist starting with campaigns. Transactional email is the well-trodden path; campaign sending has more account-level prerequisites (verified sender, opt-in proof, possible account review).
Wrap it, don't sprinkle it. Even in a small app, put a thin App\Services\BrevoNotifier (or similar) in front of the package. Every call site then depends on your interface, not on an unaudited 1.0.0 package — which matters because a 1.0.0 can churn. It also gives you one place to add logging, retries and rate limiting.
Transactional email — two patterns, pick deliberately
templateId plus a params array of merge fields. Brevo renders the HTML. Pros: content changes don't need a deploy; non-developers can iterate; consistent branding. Cons: your email content lives outside version control and isn't covered by your test suite.htmlContent/textContent. Pros: previewable, testable, in git. Cons: any copy change requires a deploy.A common split: transactional/security mail inline (you want it in git and reviewed), lifecycle/marketing-ish mail via templates.
Contacts and lists — sync, don't create
The bread-and-butter integration is "user signed up → contact exists with the right attributes and is on the right list." Model this as an idempotent upsert, not create-then-check:
email as the key, passing attributes in one payload.users table (brevo_contact_id) if you'll do updates later, though email is a fine natural key and avoids staleness when a user changes address.Drive this from a domain event + queued listener so signup isn't blocked on an HTTP round trip.
Campaigns
Model campaigns as a two-step, human-gated process: create the campaign (subject, sender, list/segment, template) in a draft state, let someone review it in the Brevo UI, then send or schedule. Do not wire "create and send" straight to a controller action — a duplicate form submission is a duplicate send to your whole list. Use schedule with an explicit UTC timestamp rather than "send now" where possible, so a mistake is cancellable.
Senders and account bootstrap
Treat sender/domain verification as a one-time setup task, not a runtime concern. Verify the sender and set up DKIM/SPF/DMARC on the domain before you write a line of send code — otherwise you'll spend an afternoon debugging a package that's working fine and a domain that isn't. A small artisan command or admin screen that lists verified senders is a worthwhile early investment.
Queue everything outbound
Every send/list-sync should be on a queue with $tries, $backoff and ShouldBeUnique where duplicate delivery is harmful. This also decouples you from Brevo latency and lets you apply a rate limiter.
Testing
Http::fake() against the package's underlying client — assert the outbound URL/method/body shape. This is your real safety net against a 1.0.x changing request formats.Mail::fake() will not catch transport-level problems if you're using the mail driver; it intercepts before the transport. Pair it with an Http::fake() test for the transport path.Thin docs at 1.0.0. Verify method names against the facade source rather than blog posts (which will mostly describe the older Sendinblue PHP SDK). If the package doesn't expose what you need, don't fight it — the underlying Brevo\Client SDK (or Http::withHeaders(['api-key' => ...]) against https://api.brevo.com/v3) is a legitimate escape hatch, ideally behind your service wrapper.
Brevo-side quirks that look like package bugs
first_name and later reading FIRST_NAME is a classic silent-no-op. Settle on one convention early and check the contact in the UI, not just the 201 response.listId => 3 is fine in dev and wrong in prod. Put list IDs in config keyed by audience name.sender must be a verified sender. Sending from an arbitrary From: address fails at the API, not at your code.Error handling. Expect an exception on non-2xx. Catch the package's exception type, and — critically — confirm you can retrieve the raw response body. Brevo returns structured errors (code, message) that tell you exactly which field is wrong; losing that in a generic RuntimeException will cost you hours. Log the body, never the API key.
Rate limits and bulk operations. Brevo enforces per-account request limits, and contact import/listing endpoints paginate (commonly limit capped around 100 with offset). A naive "loop all contacts" in a request will time out; use a chunked job with usleep/a rate limiter, and expect throttling to be a normal condition rather than an error.
Unsubscribe and compliance. Don't rely solely on Brevo's unsubscribe link in templates — if you send inline htmlContent you may still need it. And keep your users.newsletter flag and the Brevo list in sync bidirectionally via webhooks, or you'll email people who opted out.
Webhooks. The package covering "contacts/lists" doesn't guarantee webhook signature verification is built in. If you consume Brevo events (bounces, unsubscribes, delivered), validate and make your handler idempotent — retries and out-of-order delivery are normal.
Config caching. BREVO_API_KEY read at container-build time plus config:cache in deploy scripts equals "works locally, 401 in production." Add php artisan config:cache to your deploy, not your local workflow.
Multi-tenant / multi-brand. If you'll send from more than one Brevo account, don't lean on the singleton binding — check whether the package supports per-call credential overrides, or instantiate the client yourself per tenant. Retrofitting this is painful.
Version pin. Pin with a caret (^1.0) but review 1.x minor releases before deploying: for a wrapper this young, request-shape changes are the most likely breaking surface, and they may not be called out as "breaking" in semver terms.
Sandbox/test mode. Brevo has limited sandbox capability (notably for transactional SMS and some email paths). Structure your integration tests to assert on request shape against Http::fake() and reserve live calls for a manual smoke script — don't block CI on an external vendor's availability.
How can I help you explore Laravel packages today?