A Flask 3 app with POST /webhooks/revenuedot that verifies the HMAC signature of each RevenueDot webhook, ignores duplicate deliveries and acts on the event type. verify.py uses only the standard library and can be copied as is.
Status: verified. pytest (2 tests with a real signed delivery captured from a RevenueDot server, through Flask's test client) passes on Python 3.14 with Flask 3.1.3. Also tested live on 2026-09-30 with scripts/e2e-webhook.sh: a local RevenueDot signed and delivered a Test Store INITIAL_PURCHASE to python app.py 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-flask-webhook
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
pytest -q
export REVENUEDOT_WEBHOOK_SECRET=whsec_... # from the webhook you create in RevenueDot
python app.py # http://localhost:3000/webhooks/revenuedot (PORT to change)Then 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).
python app.py starts Flask's development server. In production run the same app under a WSGI server, for example gunicorn 'app:create_app("whsec_...")'.
- RevenueDot sends
POSTwithContent-Type: application/json, yourAuthorizationheader (if you set one) andX-RevenueCat-Webhook-Signature: t=<unix seconds>,v1=<hex>. See Webhooks. - The handler reads the raw body with
request.get_data()and checksv1 == HMAC-SHA256(signing_secret, "<t>.<raw body>")in constant time, and thattis within 5 minutes. Parsing and re-serialising the JSON first (request.get_json()thenjson.dumps) would change the bytes and fail the check. - It remembers
event.id, so a retried delivery is answered200without being processed twice. The example keeps ids in memory; use a table 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-fastapi-webhook: FastAPI.backend/python-django-webhook: Django.backend/node-express-webhook: Express.selfhost/docker-compose: a local RevenueDot to send the webhooks (WEBHOOK_URL=... ./seed.sh).