survos / brevo-bundle
Brevo (ex-Sendinblue) for Symfony: the official SDK on Symfony HttpClient, list/campaign commands, and webhook events re-dispatched for any listener.
Fund package maintenance!
Requires
- php: ^8.5
- getbrevo/brevo-php: ^5.0
- nyholm/psr7: ^1.8
- survos/kit-bundle: ^2.34
- symfony/config: ^8.1
- symfony/console: ^8.1
- symfony/dependency-injection: ^8.1
- symfony/event-dispatcher: ^8.1
- symfony/framework-bundle: ^8.1
- symfony/http-client: ^8.1
- symfony/http-kernel: ^8.1
- symfony/security-core: ^8.1
- twig/twig: ^3.4
Requires (Dev)
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^13.0
Suggests
- survos/tabler-bundle: Adds a "Brevo" dropdown (Account, Lists, Campaigns, Logs, Blocked) to the admin navbar.
- symfony/brevo-mailer: Send mail with MAILER_DSN=brevo+api://KEY@default, and parse Brevo webhooks.
- symfony/webhook: Receive Brevo webhooks at /webhook/brevo; this bundle re-dispatches them as BrevoMailEvent.
Provides
None
Conflicts
None
Replaces
None
README
Brevo for Symfony: the official SDK (getbrevo/brevo-php) wired onto Symfony HttpClient,
CLI commands for lists, campaigns and the blocklist, and Brevo webhooks re-dispatched as a
Symfony event any number of listeners can use.
It knows nothing about consent. Who may be pushed to a list or mailed is the caller's decision
(see survos/outreach-bundle's SendPolicy).
Install
composer require survos/brevo-bundle symfony/brevo-mailer symfony/webhook symfony/remote-event
# .env: committed, empty. Set the real key in .env.local (dev) or `dokku config:set --no-restart` (prod). BREVO_API_KEY= MAILER_DSN=brevo+api://${BREVO_API_KEY}@default
One key serves both sending (the mailer) and the SDK. Use the API transport, not
smtp-relay.brevo.com: the API returns Brevo's message id, which is what webhooks refer to.
The SDK
Brevo\Brevo is a service built on the app's http_client, so calls show in the profiler.
public function __construct(private Brevo $brevo) {} $this->brevo->contacts->getLists();
BrevoService wraps the calls apps use most: lists(), campaigns(), upsertContact(),
removeFromLists(), blockedContacts(), isConfigured(). upsertContact() never clears
Brevo's blocklist flag.
Commands
| Command | |
|---|---|
brevo:status |
account, plan, credits; says clearly when BREVO_API_KEY is empty |
brevo:lists |
contact lists with subscriber / blocklisted counts |
brevo:campaigns [--status=sent] |
email campaigns |
brevo:events [--email=] [--days=7] |
Brevo's transactional event log: requests, delivered, opened, bounces… |
brevo:blocked |
addresses blocked for transactional mail and why |
brevo:contact:push EMAIL --list=2 |
create/update a contact and add it to lists |
Brevo caps page sizes (lists 50, blocked 100, campaigns 100); the service clamps to them,
because a larger limit is a 400 that the SDK reports only as "API request failed".
BrevoService::errorMessage() recovers Brevo's real message from the exception body.
Admin pages and navbar
Read-only pages at /admin/brevo (Account, Lists, Campaigns, Logs, Blocked), straight from
the API, with links into Brevo for editing. With survos/tabler-bundle installed they appear
as a Brevo dropdown in the admin navbar (ADMIN_NAVBAR_MENU), shown only when a key is set
and the viewer has admin_role. Routes load without any import (kit-bundle's
HasConfigurableRoutes); set routes_enabled: false to drop both the pages and the dropdown.
The account's event log covers every app that shares the key, not just this one.
Webhooks
With symfony/webhook and symfony/brevo-mailer installed, the bundle routes
/webhook/brevo to Symfony's Brevo parser. It consumes the resulting remote event and
dispatches Survos\BrevoBundle\Event\BrevoMailEvent:
#[AsEventListener] public function onBrevo(BrevoMailEvent $event): void { if ($event->isSuppression) { // hard bounce, spam complaint, unsubscribe $this->suppress($event->email, $event->name, $event->messageId); } }
In Brevo: Transactional → Settings → Webhook, URL https://<host>/webhook/brevo.
To require HTTP Basic auth, put user:password@ in that URL and set
BREVO_WEBHOOK_SECRET=user:password.
- Brevo's source IPs (
1.179.112.0/20,172.246.240.0/20) are enforced. In debug mode Symfony also allows127.0.0.1, which is what lets a dev app behind a cloudflared tunnel receive them. In prod the app must trust its reverse proxy (trusted_proxies) or every call is rejected (406). - Webhooks go through Messenger when it is installed. Without a worker, use a sync transport
for
Symfony\Component\RemoteEvent\Messenger\ConsumeRemoteEventMessage. - Only transactional webhook payloads are parsed. Marketing (campaign) webhooks use a different payload and are not handled yet.
Configuration
survos_brevo: api_key: '%env(BREVO_API_KEY)%' # default; empty = not configured webhook: true # set false to leave /webhook/brevo alone admin_role: ROLE_ADMIN # pages + navbar dropdown base_template: base.html.twig # the admin pages extend this; needs a "body" block routes_enabled: true route_prefix: /admin/brevo