django-signal-webhooks
Fire webhooks automatically from Django model signals - create, update, delete, and M2M changes - with per-model admin configuration and async delivery.
Repository Health
Technical Analysis
django-signal-webhooks turns Django’s built-in post_save, post_delete, and m2m_changed signals into outbound webhook calls, letting you notify external systems whenever a model instance is created, updated, deleted, or has its many-to-many relations changed, without writing signal handlers by hand. Webhooks are configured per model through a SIGNAL_WEBHOOKS setting and managed at runtime in the Django admin, where each hook stores its endpoint, headers, an encrypted auth token, and the signals it should respond to.
Delivery runs through httpx with async fan-out across all matching hooks for an event, backed by pluggable hooks for serialization, client kwargs, error handling, and the task runner itself (threaded by default, or synchronous for tests). Optional DRF serializers and a viewset expose the webhook configuration over a REST API, and encrypted auth tokens keep credentials out of plaintext in the database.
What You Get
- Admin-managed webhook records - endpoint, headers, encrypted auth token, and per-signal enable/disable, all editable in Django admin without a deploy.
- Automatic signal wiring for post_save (create/update), post_delete, and m2m_changed (add/remove/clear), scoped per model via a SIGNAL_WEBHOOKS setting.
- Async delivery with httpx.AsyncClient, firing all matching hooks for an event concurrently and recording last_success/last_failure/last_response per hook.
- Encrypted auth tokens (cipher-key based) so webhook credentials aren’t stored in the database as plaintext.
- Optional DRF integration - a ModelViewSet and serializer to manage webhooks over a REST API instead of only the admin panel.
Common Use Cases
- Syncing a Django app’s User or Order model changes to an external CRM or data warehouse in near real time.
- Notifying a Slack, Discord, or internal service whenever specific model records are created or deleted, without polling.
- Triggering downstream cache invalidation or search-index updates when many-to-many relations change.
- Letting non-developers add or remove webhook destinations for a SaaS integration directly from the Django admin.
Under The Hood
Architecture The package separates concerns cleanly across handlers.py, models.py, settings.py, and utils.py: Django’s post_save, post_delete, and m2m_changed signals are wired once at import time via @receiver, and each event is routed through find_hook_handler against a per-model, per-signal HOOKS mapping defined in the SIGNAL_WEBHOOKS setting. Delivery itself runs in fire_webhooks, which fans out an asyncio.Task per matching hook against a shared httpx.AsyncClient and consumes them as they complete through a small custom async generator built on an asyncio.Queue, then persists per-hook success/failure/response state with a single bulk_update wrapped in sync_to_async. Every major seam - serialization, client kwargs, additional query filtering, error handling, and even the task runner that starts a hook - is a swappable, dotted-path setting resolved through django-settings-holder, so the default threaded dispatch can be replaced with a synchronous handler for tests without touching library code.
Tech Stack Built for Python 3.11 through 3.14 and Django 5.0 through 6.0, using httpx for outbound requests, asgiref’s sync_to_async for bridging the async delivery path back to the ORM, the cryptography package to cipher stored auth tokens, and django-settings-holder for the app’s Django-settings-driven configuration pattern. Django REST Framework is an optional extra that unlocks a ModelViewSet and serializer for managing webhooks over an API. The project is packaged with Poetry, documented with MkDocs (including a Mermaid plugin for diagrams), tested across environments with nox, and linted with an extensive Ruff ruleset covering bugbear, security, complexity, and style rules.
Code Quality The test suite spans admin, API, hook-dispatch, local-settings, and utility behavior with pytest-django and freezegun for time-sensitive assertions, exercising both the default configuration and custom per-model, per-signal overrides. The codebase is fully type-annotated with TYPE_CHECKING-guarded imports to avoid runtime cost, ships a py.typed marker for downstream type checkers, and enforces its Ruff rules plus coverage tracking through GitHub Actions CI, Coveralls, and Dependabot for dependency upkeep.
API Design Getting started is three steps - add the app, run migrations, and declare a SIGNAL_WEBHOOKS.HOOKS mapping - after which webhook destinations themselves are managed entirely from the Django admin with no further code changes. The settings API uses an ellipsis sentinel to mean “use the default handler” versus None for “explicitly disabled,” which is a compact convention once learned but not immediately obvious from the type alone; the docs site and inline setting comments largely offset that with a worked example of every override point.