A minimal Django 5 project with one view, POST /webhooks/revenuedot, that verifies the HMAC signature of each RevenueDot webhook, ignores duplicate deliveries and acts on the event type. It needs no database. webhooks/verify.py uses only the standard library and can be copied as is; webhooks/views.py is the view to drop into an existing project.
Status: verified. python manage.py test (2 tests with a real signed delivery captured from a RevenueDot server, through Django's test client) passes on Python 3.14 with Django 5.2.17. Also tested live on 2026-09-30 with scripts/e2e-webhook.sh: a local RevenueDot signed and delivered a Test Store INITIAL_PURCHASE to manage.py runserver and recorded delivered, HTTP 200.
RevenueDot is the open-source, self-hostable alternative to RevenueCat. It speaks the same API as the RevenueCat SDKs, so apps switch by setting one proxy URL. Its webhooks use RevenueCat's payload shape, so a handler written for RevenueCat keeps working.
cd backend/python-django-webhook
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
python manage.py test
export REVENUEDOT_WEBHOOK_SECRET=whsec_... # from the webhook you create in RevenueDot
python manage.py runserver 3000 # http://localhost:3000/webhooks/revenuedotThen create the webhook on your RevenueDot server with url set to http://host.docker.internal:3000/webhooks/revenuedot when RevenueDot runs in docker compose up from selfhost/docker-compose. WEBHOOK_URL=http://host.docker.internal:3000/webhooks/revenuedot ./seed.sh there creates the webhook and prints its signing secret (see Webhooks).
runserver is for development. In production serve revenuedot_webhook.wsgi with gunicorn or similar, and set DJANGO_SECRET_KEY and DJANGO_ALLOWED_HOSTS.
To add the webhook to your own project, copy the webhooks folder, add "webhooks" to INSTALLED_APPS, add the two REVENUEDOT_* settings from revenuedot_webhook/settings.py and route webhooks/revenuedot to revenuedot_webhook.
- RevenueDot sends
POSTwithContent-Type: application/json, yourAuthorizationheader (if you set one) andX-RevenueCat-Webhook-Signature: t=<unix seconds>,v1=<hex>. See Webhooks. - The view reads the raw body from
request.bodyand checksv1 == HMAC-SHA256(signing_secret, "<t>.<raw body>")in constant time, and thattis within 5 minutes. Parsing and re-serialising the JSON first would change the bytes and fail the check. The view is@csrf_exemptbecause RevenueDot sends no CSRF token; the signature is what authenticates it. - It remembers
event.id, so a retried delivery is answered200without being processed twice. The example keeps ids in memory; use a model with a unique index in production. - It answers HTTP 200. RevenueDot counts only 200 as delivered; any other status or a timeout (60 seconds) is retried after 5, 10, 20, 40 and 80 minutes, then marked failed. Failed deliveries can be retried from the dashboard or with
POST /v2/projects/{project_id}/webhooks/{webhook_id}/deliveries/{delivery_id}/retry. - Event types and payloads: Webhook events. For the current state of a customer, call
GET /v1/subscribers/{app_user_id}with a secret key rather than rebuilding it from events (seebackend/check-entitlement-python).
RevenueDot sends the same JSON body ({"api_version": "1.0", "event": {...}}) and the same Authorization header you configure, so an existing RevenueCat handler keeps working. Things to add or check:
- Verify
X-RevenueCat-Webhook-Signature. RevenueDot signs every delivery with HMAC-SHA256 over"<t>.<raw body>". If your RevenueCat handler only checked theAuthorizationheader, add the signature check from this example; theAuthorizationcheck can stay. - Point the webhook at this handler in RevenueDot (dashboard: Integrations > Webhooks, or
POST /v2/projects/{project_id}/integrations/webhooks) and copy thesigning_secretfrom the response: it is shown only once. - Dedupe on
event.id. Delivery is at least once, like RevenueCat's.
- Webhooks guide: setup, signature verification in Node, Python, Go, Ruby, PHP and C#, retries.
- Webhook events: every event type with a real payload.
- Authentication: secret keys for server-side calls.
backend/python-flask-webhook: Flask.backend/python-fastapi-webhook: FastAPI.backend/check-entitlement-python: check a customer's entitlement from Python.selfhost/docker-compose: a local RevenueDot to send the webhooks (WEBHOOK_URL=... ./seed.sh).