Skip to content

Latest commit

 

History

History

python-flask-webhook

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

Receive RevenueDot webhooks with Flask

What this is

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.

Why RevenueDot

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.

Run it

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_...")'.

How it works

  1. RevenueDot sends POST with Content-Type: application/json, your Authorization header (if you set one) and X-RevenueCat-Webhook-Signature: t=<unix seconds>,v1=<hex>. See Webhooks.
  2. The handler reads the raw body with request.get_data() and checks v1 == HMAC-SHA256(signing_secret, "<t>.<raw body>") in constant time, and that t is within 5 minutes. Parsing and re-serialising the JSON first (request.get_json() then json.dumps) would change the bytes and fail the check.
  3. It remembers event.id, so a retried delivery is answered 200 without being processed twice. The example keeps ids in memory; use a table with a unique index in production.
  4. 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.
  5. 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 (see backend/check-entitlement-python).

Migrate from RevenueCat

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 the Authorization header, add the signature check from this example; the Authorization check can stay.
  • Point the webhook at this handler in RevenueDot (dashboard: Integrations > Webhooks, or POST /v2/projects/{project_id}/integrations/webhooks) and copy the signing_secret from the response: it is shown only once.
  • Dedupe on event.id. Delivery is at least once, like RevenueCat's.

Docs

Related examples