eventhomes/laravel-mandrillhooks
Laravel package for handling Mandrill webhooks. Receive and process Mandrill events such as bounces, opens, clicks, and spam reports in your Laravel app, with ready-to-use routes, controllers, and event handling.
1. Require the package
composer require eventhomes/laravel-mandrillhooks
Check composer.json of the package first — this was last released in March 2020, so confirm the illuminate/support constraint matches your Laravel version. On Laravel 9+ you may need --with-all-dependencies or a fork.
2. Publish/verify the service provider
The package ships a MandrillHooksServiceProvider. On Laravel 5.5+ it's auto-discovered; on older versions register it in config/app.php. There's no meaningful config to publish.
3. Create your webhook controller
Extend the package's base controller (Eventhomes\LaravelMandrillHooks\MandrillWebhookController) in app/Http/Controllers/. This base class handles normalizing the incoming Mandrill batch and routes each event to a per-event hook method you override. Open the base class first — it's the single most important file to read, since it defines exactly which methods (handleBounce, handleReject, handleSpam, handleUnsub, etc.) get called and what shape $payload has.
4. Register the route
Mandrill POSTs a JSON array of events, so the route must be excluded from CSRF:
routes/api.php (no CSRF middleware), or$except in app/Http/Middleware/VerifyCsrfToken.php.Route::post('/webhooks/mandrill', [MandrillController::class, 'handle']);
5. Point Mandrill at it
In the Mandrill dashboard (Settings → Webhooks), set the URL to your public HTTPS endpoint and select the events you want (bounce, reject, spam, unsub, etc.). Verify it's reachable via curl/ngrok before wiring real traffic.
First use case: catch hard bounces. Override handleBounce($payload), log $payload['msg']['email'], and suppress it in your own user table.
Override only the hooks you care about. The base controller provides no-op defaults for every Mandrill event type, so you opt in per event. Keep each override short — parse, dispatch, return.
Fan-out to application events/jobs. The clean pattern is: controller receives → dispatches a domain event (e.g. EmailBounced) → queued listener does the work. Keeps the webhook response fast and lets you retry your own logic independently of Mandrill's retries.
public function handleBounce($payload)
{
event(new EmailBounced(
$payload['msg']['email'],
$payload['msg']['bounce_description'] ?? null,
$payload['msg']['_id'] ?? null,
));
}
Idempotency via msg._id. Mandrill retries non-200 responses, so guard against duplicate processing with firstOrCreate/upsert keyed on $payload['msg']['_id'] (the outbound message id) or $payload['_id'] (the event id).
Batch handling. Each webhook call contains an array of events, not one. The base controller loops for you, but any state you keep must be per-event. Don't assume one bounce per request.
Raw capture for debugging. Wrap the handler with middleware or a try/catch that writes the raw body to a mandrill_webhook_log table. When something silently "isn't firing," 90% of the time it's a 500 in your hook or a mis-selected event in the dashboard — having the raw payload is how you tell the difference.
Testing locally. Post a real captured payload:
curl -X POST https://your-app.test/api/webhooks/mandrill \
-H "Content-Type: application/json" \
-d @tests/fixtures/mandrill-bounce.json
Keep fixtures for each event type you handle; they double as regression tests.
No signature verification out of the box. Mandrill signs webhooks with X-Mandrill-Signature (HMAC-SHA1 of the URL plus sorted POST params, using the webhook's auth key). This package's core job is dispatch, not authentication — if the base controller doesn't verify it, add a middleware that does, or anyone can forge bounce events into your system (useful for suppressing competitor emails).
Return 200 fast and do the work async. Mandrill treats non-2xx as failure and retries with backoff, and will eventually disable the webhook. Never do synchronous DB-heavy work (or Mail::send) directly inside a hook; push to a queue.
Package is EOL-adjacent (2020, 0.67 score, 62 stars). Expect friction: no typed properties, no Laravel 10/11 support declared, possibly no PHP 8.2 compatibility. If you're on modern Laravel, budget an hour to either fork+patch the composer.json constraint or reimplement the ~100-line dispatcher yourself — it's genuinely that small.
Payload shape differs between outbound events and inbound. Outbound (hard_bounce, reject, spam, unsub) key off msg.email and msg._id; inbound events have a different structure. Don't reuse one parser for both.
Mandrill's unsub doesn't mean your app unsubscribed. It means the recipient clicked the Mandrill-hosted unsubscribe link. If you have your own subscription model, reconcile it — otherwise you'll keep mailing people who've opted out.
Soft bounce vs hard bounce. soft_bounce is transient (mailbox full); hard_bounce is permanent (invalid address). Only suppress on hard bounce, spam, and reject — suppressing on soft bounces will silently lose deliverable email.
Empty dashboard subscription list. If no events are checked in Mandrill, the webhook still fires — with an empty array. Your handle() will return 200 and look successful while doing nothing. Always test with a real triggering event in the dashboard.
No retry on your side. The package doesn't persist failures anywhere. If your handler throws, the event is gone once Mandrill gives up. Pair it with the raw-capture log mentioned above so you can replay.
How can I help you explore Laravel packages today?