stapel-webhooks
The reaction layer. Your modules already emit facts — user.registered,
listing.published, payment.completed — as schema-typed comm Actions
riding a transactional outbox. This is the consumer side: a declarative
"when X happens, deliver it to Y" that the app layer and external systems
can both use.
comm Action (already there)
│
▼ subscription registry + matcher
├─► webhook HTTP POST, HMAC-signed, retried, dead-lettered
├─► notification email/push/SMS via stapel-notifications
├─► ws live frame on a realtime stream
└─► custom allowlisted dotted path in your app
Part of the Stapel framework.
Install
pip install stapel-webhooks
INSTALLED_APPS = [..., "stapel_webhooks"]
path("webhooks/", include("stapel_webhooks.urls")) # -> /webhooks/api/v1/...
Then schedule the drain — with the default deferred dispatch, nothing is delivered until it runs:
from stapel_webhooks.tasks import get_webhooks_beat_schedule
CELERY_BEAT_SCHEDULE = {**get_webhooks_beat_schedule(), ...}
…or from cron: manage.py deliver_webhooks. (A system check tells you if you
forget.)
Subscribe to something
POST /webhooks/api/v1/subscriptions
{
"event_type": "listing.published",
"delivery": "webhook",
"target": {"url": "https://crm.example.com/hooks/stapel"},
"filter": {"city": "berlin", "price": {"$gte": 500}}
}
→ 201 {"id": "3fa8…", "secret": "whsec_…"}
The secret is returned once, on creation (and again only on rotation) — like an API key.
What is subscribable
Not a list somebody maintains: every event any installed module ships a
schema for, scanned out of schemas/emits/.
python manage.py webhooks_event_catalog
Install a module and its facts become subscribable. Uninstall it and they stop being offered. No release of this package either way.
What your receiver gets
X-Stapel-Delivery: 9aa1c0de-… ← identical across retries; de-duplicate on it
X-Stapel-Event: listing.published
X-Stapel-Signature: t=1755993600,v1=6f1e…c3
{"id":"9aa1…","type":"listing.published","created_at":"…","data":{ … }}
Verify: HMAC-SHA256(secret, f"{t}.{raw_body}"), compared in constant time
against the raw bytes — and refuse a t outside your tolerance window,
which is the replay guard. In Python, stapel_webhooks.verify(secret, body, header) does it for you and accepts a list of secrets so rotation has an
overlap.
When the receiver is down
Exponential backoff with a cap and jitter (8 attempts over ~2 h by default), then a dead letter — a row that keeps the payload and can be replayed from the API with the full ladder again. A 4xx that is not 408/425/429 skips the ladder entirely: eight identical refusals teach nobody anything. A rule that keeps dying deactivates itself and says so on the bus.
Closed by default
- webhook targets must be https and must resolve to a public address — private, loopback, link-local and the cloud metadata endpoint are refused, with no second DNS lookup between check and connect;
- delivery does not happen on your request thread;
- the
customdelivery type — a dotted path named by a database row — is unusable until you allowlist your handlers, and can be removed outright.
Each of those has a system check that reports it if you open it.
Documentation
MODULE.md— the integration contract: HTTP surface, comm surface, the delivery state machine, every extension point, and the known limitations.CONFIG.MD— every setting, and the four that are decisions rather than tuning.
License
MIT
Metadata
Release files for stapel-webhooks 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| stapel_webhooks-0.1.2.tar.gz | 96.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| stapel_webhooks-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 177.9 kB
Release files / stapel_webhooks-0.1.2.tar.gz
| Download URL | stapel_webhooks-0.1.2.tar.gz |
|---|---|
| Size | 96.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
feb3f5be2ae63cf9594a01ec2b09347cd08b7223d332142e0494497b632b8e9e
|
|
BLAKE2b-256 checksum How to use checksums |
6f8703375c6a539574359a634b08a23c4106d274864a570b1448c741a2b05cd1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.
Transparency logRelease files / stapel_webhooks-0.1.2-py3-none-any.whl
| Download URL | stapel_webhooks-0.1.2-py3-none-any.whl |
|---|---|
| Size | 81.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
17e87f670b8f469c92db4540cbff7b6baa902572c489007e9bf8e74927c89960
|
|
BLAKE2b-256 checksum How to use checksums |
4e4363b17d7a4a266e24a629b6491d9d4121752096b37ba3532c5370df4a751b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.
Transparency log