Skip to content

Repository files navigation

CPA Postback Processor

FastAPI project that models a small CPA / affiliate tracking workflow: register clicks, ingest partner postbacks, keep webhook processing idempotent, and expose a lightweight dashboard for manual verification.

Highlights

  • Thin FastAPI routes with service-layer business logic
  • SQLite with transactions, foreign keys, WAL mode, and targeted indexes
  • Monetary values stored as integer cents instead of floats
  • Webhook processing keyed by a stable external_ref
    • prefers transaction_id for lifecycle updates
    • falls back to event_id when no transaction identifier is present
  • Duplicate retries are ignored, while legitimate lifecycle updates are applied in place
  • Multi-currency-safe stats via explicit currency breakdowns
  • Admin endpoints support optional token protection
  • Webhook ingress can be protected with either a shared secret header or an HMAC body signature
  • Webhook payload previews are redacted before they reach the UI
  • API tests, Docker, and GitHub Actions CI included

Tech stack

  • FastAPI
  • SQLite
  • Pytest
  • Ruff
  • Docker / Docker Compose

Architecture

  • app/main.py wires the FastAPI surface and lifecycle.
  • app/manual_flows.py owns operator-driven click and manual conversion creation.
  • app/postback_flows.py owns webhook ingestion, duplicate handling, and lifecycle updates.
  • app/reporting.py owns dashboard stats and webhook-log formatting.
  • app/service_helpers.py keeps money parsing, status normalization, and payload helpers out of the route layer.

Project structure

cpa-postback-processor/
├── .env.example
├── .github/workflows/ci.yml
├── Dockerfile
├── docker-compose.yml
├── app.py
├── app/
│   ├── __init__.py
│   ├── config.py
│   ├── db.py
│   ├── demo.py
│   ├── main.py
│   ├── schemas.py
│   ├── security.py
│   ├── manual_flows.py
│   ├── postback_flows.py
│   ├── reporting.py
│   ├── service_helpers.py
│   ├── services.py
│   └── utils.py
├── docs/
│   └── engineering-decisions.md
├── static/
│   ├── app.css
│   ├── app.js
│   └── index.html
├── tests/
│   ├── conftest.py
│   └── test_api.py
├── LICENSE
├── Makefile
├── pyproject.toml
├── requirements-dev.txt
└── requirements.txt

Quick start

python -m venv .venv
source .venv/bin/activate  # Windows: .venv\Scripts\activate
pip install -r requirements.txt -r requirements-dev.txt
uvicorn app:app --reload --port 8001

Open http://127.0.0.1:8001.

Docker

docker compose up --build

Environment variables

Variable Purpose Default
APP_ENV development, test, or production development
APP_DATA_DIR directory for SQLite files ./data
APP_DB_PATH explicit SQLite database path ./data/cpa_tracker.db
APP_STATIC_DIR static frontend assets ./static
DEMO_SEED_ENABLED seed sample rows on startup for local review true
DEMO_RESET_ENABLED enable the local sample-data reset endpoint true in development, false in production
CORS_ORIGINS comma-separated allowlist *
ADMIN_TOKEN when set, protects admin endpoints via X-Admin-Token empty
WEBHOOK_SHARED_SECRET when set, requires X-Webhook-Secret on inbound postbacks empty
WEBHOOK_HMAC_SECRET when set, validates a request-body HMAC signature empty
WEBHOOK_SIGNATURE_HEADER header name used for the HMAC signature X-Signature
WEBHOOK_SIGNATURE_PREFIX prefix prepended to the expected HMAC digest sha256=
WEBHOOK_MAX_BODY_BYTES max webhook request size 65536

API overview

Public endpoints

  • GET /api/health
  • GET /api/meta
  • POST /api/clicks
  • POST /api/conversions
  • POST /api/webhooks/postback
  • GET /api/stats

Admin endpoints

  • GET /api/webhooks
  • POST /api/demo/reset (restores the local sample dataset)

Admin behavior depends on ADMIN_TOKEN:

  • when ADMIN_TOKEN is set, send it in X-Admin-Token
  • when ADMIN_TOKEN is not set, admin routes remain available in non-production environments only

Example webhook

curl -X POST http://127.0.0.1:8001/api/webhooks/postback \
  -H "Content-Type: application/json" \
  -d '{
    "source": "network-a",
    "click_id": "<existing click id>",
    "transaction_id": "txn-1001",
    "status": "approved",
    "payout": 4.25,
    "revenue": 7.80,
    "currency": "USD"
  }'

The processor prefers transaction_id as the stable external_ref. If a partner does not send one, it falls back to event_id. Repeated retries with the same stable reference are ignored when nothing changed. If the same reference arrives with a new status or amount, the existing conversion is updated instead of duplicated.

Dashboard coverage

  • Manual click creation
  • Real webhook ingestion
  • Duplicate retry handling
  • Status / payout updates for the same transaction
  • Currency-safe analytics
  • Redacted webhook previews
  • Token-protected admin log access when enabled

Running tests and lint

ruff check .
pytest

Or use the Makefile targets:

make lint
make test

See docs/engineering-decisions.md for the service split, idempotency trade-offs, and the local data-reset rationale.

Design decisions

Why integer cents?

Financial values stored as floats eventually drift. Integer cents keep arithmetic deterministic and make aggregation logic easier to review.

Why external_ref in addition to event_id?

Partners are inconsistent: some send a stable transaction identifier, some send only an event identifier, and some send multiple events for the same transaction over time. The schema keeps a stable idempotency key separate from the displayed event identifier so lifecycle updates can be applied without duplicating conversions.

Why currency breakdowns instead of a single payout total?

Aggregating USD and EUR into one total is wrong. The API returns payout and revenue per currency so reporting stays honest.

Why SQLite here?

SQLite keeps setup friction low for a self-contained local project. For higher write volume, the next step would be PostgreSQL plus background webhook processing.

Scaling notes

Reasonable next steps for a higher-throughput version:

  • PostgreSQL instead of SQLite
  • queue-backed or async webhook workers
  • structured logging and metrics
  • partner-specific signature verification policies
  • stronger admin authentication than a single static token

About

Backend service for processing affiliate postbacks with idempotent event handling

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages