Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Mailgunwebhooks Bundle Laravel Package

azine/mailgunwebhooks-bundle

Symfony bundle that captures Mailgun webhook event data in your database, so you can search, filter, and display delivery logs beyond Mailgun's 48-hour retention, delete old events via CLI, and get alerts for blacklisted sending IPs or spam-related bounces.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check before you start: this is a Symfony bundle, not a Composer package you can use inside a Laravel app. AzineMailgunWebhooksBundle ships a bundle class, Doctrine entities, Twig templates, a DI extension, and console commands wired through AppKernel/bundles.php. Day-to-day leverage as a Laravel developer comes in one of two shapes:

  1. Satellite Symfony app (most realistic): run a small Symfony 7.4 service that owns the mailgun_event table, point Mailgun webhooks at it, and have your Laravel app read that table (shared DB) or call an internal API for delivery history.
  2. Reference implementation: copy the webhook controller + entity + listener pattern into a Laravel route/command if you must stay in one framework. You lose the admin UI and CLI for free.

Assuming option 1, minimal path:

composer require azine/mailgunwebhooks-bundle
// config/bundles.php
Azine\MailgunWebhooksBundle\AzineMailgunWebhooksBundle::class => ['all' => true],
# config/routes.yaml
azine_mailgun_webhook:
    resource: "@AzineMailgunWebhooksBundle/Resources/config/routing/mailgunevent_webhook.yml"
    prefix: /
azine_mailgun_admin:
    resource: "@AzineMailgunWebhooksBundle/Resources/config/routing/mailgunevent_admin.yml"
    prefix: /admin/
# config/packages/azine_mailgun_webhooks.yaml
azine_mailgun_webhooks:
    api_key: '%env(MAILGUN_API_KEY)%'
    public_api_key: '%env(MAILGUN_PUBLIC_KEY)%'
    email_domain: '%env(MAILGUN_DOMAIN)%'
    no_reply_email: '[email protected]'
    no_reply_name: 'Example Mailer'
    spam_alerts:
        enabled: true
        interval: '60'
        alerts_recipient_email: '%env(MAILER_ALERT_TO)%'

Then generate the schema (the bundle's entity needs a table — check for bundled migrations first and otherwise doctrine:migrations:diff):

php bin/console doctrine:schema:update --dump-sql   # inspect before --force

First use case, in order:

  1. php bin/console debug:router -e prod | grep mailgunevent_webhook — copy that absolute URL.
  2. Paste it into every hook on https://app.mailgun.com/app/webhooks (delivered, bounced, dropped, opened, clicked, complained, unsubscribed).
  3. Click Mailgun's "Test" button and confirm a row lands in the admin list (/admin/, find it with debug:router | grep mailgun_overview).
  4. Send a real transactional email and watch the event appear — that's your proof the whole loop works before you rely on it.

Do step 3 in the prod env specifically; dev URLs and trailing slashes are the #1 cause of "webhooks aren't arriving".

Implementation Patterns

Single receiver, many event types. Mailgun posts every event type to the same endpoint; the bundle normalizes them into one MailgunEvent row. Don't create per-event-type routes — configure the same URL everywhere and filter at query time.

React to events with a listener. Every persisted event dispatches a MailgunWebhookEvent carrying the MailgunEvent. This is your hook for anything downstream — Slack on dropped, tagging a Message-Id against a user, pushing complaint events into a suppression list:

class BounceToSlackListener implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [MailgunWebhookEvent::class => 'onWebhook'];
    }

    public function onWebhook(MailgunWebhookEvent $event): void
    {
        $mailgunEvent = $event->getMailgunEvent();
        // inspect event type / recipient / message headers, dispatch to Slack, tag in CRM, etc.
    }
}

Keep listeners fast and idempotent — Mailgun retries, and a slow listener blocks the webhook response (Mailgun times out and retries, which amplifies load).

Troubleshooting as the daily workflow. The highest-value day-to-day use is answering "did this user actually get the password reset / invoice / newsletter?" Search the admin list by recipient and open the single-event detail. Because Mailgun free-tier purges logs after ~48h, this is the only place that answer exists a week later — so deploy this before you need it, not after an incident.

Cron the two commands. Both are designed for a scheduler:

# weekly — keep the table bounded, strtotime-style relative date
php bin/console mailgun:delete-events --date '60 days ago'

# every few hours — blacklist check with retries before alerting
php bin/console mailgun:check-ip-in-blacklist --numberOfAttempts 5

Set repeat_notification_after_days to a non-zero value (e.g. 3) so a persistent listing doesn't page your on-call every run; you only get re-notified when the blacklist set actually changes or the mute window expires. Note the HetrixTools free plan allows roughly 3 checks/day — for most teams this is an hourly-or-slower cron, not every five minutes.

Deliverability monitoring as a separate signal. spam_alerts handles the "Mailgun told us someone complained" path (with the helpdesk ticket config for requesting a clean IP), and hetrixtools_service handles "our sending IP got blocklisted". Wire both to an inbox your team actually reads, and set interval to avoid alert-flapping during a complaint burst.

Reporting/queries. For engagement numbers, query the bundle's repository directly from a controller or a Laravel-side read model: group by event type over a date range. On 5.0.0, replay/deduplication is handled at ingest, so open/click rates aren't inflated by Mailgun's retry deliveries — that's the version where these numbers become safe to put in front of stakeholders.

Integrate with your Laravel app. Simplest: shared database, read-only Eloquent model pointed at mailgun_event. Cleaner: expose a small /internal/events/{messageId} endpoint on the Symfony side and let Laravel call it. Either way, keep the webhook endpoint on the Symfony host so signature keys and the admin route stay in one place.

Gotchas and Tips

  • Framework mismatch is the real cost. Budget for standing up and operating a second app, or for a port. If you're on Laravel 11+, a 100-line MailgunWebhookController + signature check + mailgun_events migration probably beats running a Symfony satellite — evaluate honestly before committing to a second runtime.
  • -date in the README is stale. Modern Symfony Console parses -date as a cluster of short options. Use --date '60 days ago'. If the command errors with "option does not exist", this is why.
  • Firewall and CSRF. The webhook route must be reachable anonymously and excluded from CSRF. If your Symfony firewall requires auth on /, Mailgun gets redirected to a login page and you'll see zero rows with a 200-ish response — classic silent failure. Verify with curl -X POST from outside your network.
  • Signature validation needs the right key. Mailgun's HTTP webhook signing key is not necessarily the same credential as your private API key. If 5.0.0's strict validation rejects legitimate events, check which key the bundle's config maps to (api_key) and whether your Mailgun account has a distinct webhook signing key. A forged-event-free store is worth the setup pain — but only once it's actually accepting real traffic.
  • Don't 5xx on malformed input. Mailgun retries failed deliveries for hours. Returning 500 for a garbage payload creates a retry storm; invalid signatures should be rejected quickly with a non-retryable response, and errors should not leak internals (5.0.0's "safe error responses" address the latter). Confirm the behavior you want and test it with a deliberately bad request.
  • Use the prod router for the URL. debug:router -e prod, not dev. Dev URLs, ports, and trailing slashes all produce "webhook configured, nothing stored".
  • Table growth is real. Opened/clicked events from any real list dwarf delivered events. Add an index on message/recipient identifiers and the timestamp column, and don't set the retention window to "forever" unless you've measured the insert rate. The delete command is your friend.
  • Monolog + Mailgun can burn your quota. If Monolog emails errors through Mailgun, scanner-generated 404s become real sends and real webhook events. Use excluded_404s regexes as the README suggests; otherwise your free-tier 10k limit disappears into /cgi-bin/php probes.
  • Dedup only exists as of 5.0.0. On older versions you must dedupe by Message-Id + event type + timestamp in your reporting queries, or your numbers will drift high. Treat the version pin as a reporting-correctness decision, not a cosmetic one.
  • Notification retries are bounded and observable in 5.0.0 — failed alert sends are visible rather than silently swallowed. Check that surface during your first week; if alert delivery is failing you want to know before an actual deliverability incident.
  • Schema management is on you. Don't run doctrine:schema:update --force in prod; generate a migration from the bundle's entity and ship it through your normal pipeline.
  • Extension points: bundle template override for the admin views, a custom listener replacing the bundled mailer listener (SwiftMailer-era listener is gone in the Symfony Mailer world — don't copy old blog snippets), and config-level tuning of repeat_notification_after_days, interval, and web_view_token before you touch code.
  • Low-traffic signal. 10 stars and zero dependents means few people have hit these edges before you. Read the bundle's source for the entity and controller before debugging blind — it's small, and the README's "ToDos" (more tests, CSS, cleanup commands) tells you where rough spots remain.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin