---
title: "Real-time revenue with GTM, Flask and MySQL"
url: "https://www.omnicliq.com/en/blog/real-time-revenue-tracking-gtm-flask-mysql/"
description: "In this guide we walk through, step by step, how to set up a complete revenue tracking system using Google Tag Manager (web) and sending data through a Server-Side GTM container, so that every purchase lands in a MySQL database and you know your e-shop revenue in real time. The goal is not to build yet another report — it is to have an independent purchase record in your own database, so the real revenue stays visible even if GA4, Google Ads, Meta Ads, or any other tool runs into a problem."
category: "data-bi"
datePublished: "2025-11-24"
readTime: "6 min read"
author: "Dimitris Bachtsevanis"
locale: "en"
---

# Real-time revenue with GTM, Flask and MySQL

> In this guide we walk through, step by step, how to set up a complete revenue tracking system using Google Tag Manager (web) and sending data through a Server-Side GTM container, so that every purchase lands in a MySQL database and you know your e-shop revenue in real time. The goal is not to build yet another report — it is to have an independent purchase record in your own database, so the real revenue stays visible even if GA4, Google Ads, Meta Ads, or any other tool runs into a problem.

**TL;DR.** A step-by-step walkthrough of building a real-time revenue tracking pipeline that works independently of GA4, Meta, or any ad platform. Web GTM → Server-Side GTM → Flask API → MySQL. Purchases land in your own database within milliseconds, ready for real-time dashboards and budget decisions that can't wait for platform lag.

## Why MySQL and not GA4 or BigQuery?

[GA4](/en/labs/ga4/) is excellent for analytics, but it has one fundamental drawback: it is slow. During high-volume periods like Black Friday, reporting lag can drive the wrong decisions. When ad spend needs to react in real time, an independent logging system stops being a nice-to-have and becomes essential.

The Web [GTM](/en/labs/gtm/) → [Server-Side GTM](/en/business-intelligence/data-collection/) → MySQL pipeline:
- captures every purchase the moment it happens
- stores the data on your own server
- operates independently of any analytics tool
- is ideal for real-time dashboards and immediate budget decisions

Storing orders in MySQL gives you several advantages:
- **Real revenue in milliseconds** from the moment the purchase happens.
- **No dependency** on GA4 latency or sampling.
- **Your own dashboards** in Power BI, Looker Studio, or Metabase.
- **Easy integration** with custom tools, scripts, or alerts.
- **Reconciliation** against ERP or site orders.
- **Free:** entire pipeline runs on open-source + your own infrastructure.

The outcome: the marketing and performance team gets revenue as it actually happens, with no delays and no dependencies.

## Architecture overview

The system is built on three layers, each with a clear role:

1. **Browser / dataLayer:** emits the purchase event with all order details.
2. **GTM Web Container:** reads the dataLayer and forwards data to Server-Side GTM.
3. **Server-Side GTM → MySQL API:** processes, enriches, and stores purchases in the database.

Data flow:

```
Browser (dataLayer.purchase)
   ↓
GTM Web (Tag: Send to Server)
   ↓
Server-Side GTM (Client + Tag)
   ↓
REST API Endpoint (Flask / Node / PHP)
   ↓
MySQL (orders table)
```

Every layer does exactly what it should — nothing more, nothing less. When the architecture is clean, management, debugging, and extension all become much simpler.

## The dataLayer is the foundation

Before any GTM work (web or server-side), the e-shop must emit a well-structured purchase event into the dataLayer. Without a clean and complete event, the entire pipeline is built on half-truths — which ends in wrong revenue numbers and hard-to-debug errors.

Recommended purchase event structure (GA4 Enhanced E-commerce format):

```javascript
dataLayer.push({
  event: "purchase",
  ecommerce: {
    transaction_id: "ORDER12345",
    value: 74.90,
    currency: "EUR",
    tax: 0,
    shipping: 3.90,
    discount: 5.00,
    coupon: "BF2025",
    items: [
      {
        item_id: "SKU-00123",
        item_name: "Oversized Hoodie",
        item_brand: "BrandX",
        item_category: "Apparel > Women > Hoodies",
        item_variant: "Black / Medium",
        price: 39.95,
        quantity: 1
      }
    ]
  }
});
```

Common dataLayer problems to fix at the e-shop level first:
- missing `order_id`
- missing `coupon`
- no separation of `subtotal`, `discount`, and `tax`
- missing item-level fields (brand, category, variant)
- event fires at the wrong moment (refresh, back navigation, SPA transitions)

Fix these upstream. The dataLayer is the “data contract” that feeds every downstream layer.

## Web GTM → Server-Side GTM

In Web GTM we deliberately avoid forwarding the entire dataLayer to Server-Side GTM — that inflates processing and transfer cost for no benefit. Instead, we create Data Layer Variables for the specific fields we need (`transaction_id`, `value`, `currency`, `shipping`, `tax`, `discount`, `items`) and send only those via a Stape Data Tag.

The tag triggers only on the `purchase` event, so every order is sent once to Server-Side GTM — no duplication, no wasted calls.

On the server side, the Data Client receives the incoming variables and converts them into an event inside the Server-Side GTM container. In Debug Mode you see one new event per purchase, containing exactly the fields we forwarded — `transaction_id`, `value`, `currency`, `shipping`, `tax`, `discount`, and the full items list. No transformations needed; values arrive as-is.

## Server-Side GTM → Flask endpoint

Server-Side GTM forwards the purchase to our own backend via a JSON HTTP Request tag pointing at something like `https://api.mydomain.gr/gtm/purchase-log`. The payload carries only the fields we need — transaction_id, value, currency, shipping, tax, discount, items list, and basic context (timestamp, domain).

Minimal Flask app that receives and writes to MySQL:

```python
from flask import Flask, request, jsonify
import mysql.connector
from datetime import datetime
import json

app = Flask(__name__)

db_config = {
  "host": "localhost",
  "user": "my_user",
  "password": "my_password",
  "database": "my_database",
  "charset": "utf8mb4"
}

@app.route("/gtm/purchase-log", methods=["POST"])
def purchase_log():
  data = request.get_json(silent=True) or {}
  order_id = data.get("transaction_id")
  value    = data.get("value")

  if not order_id or value is None:
    return jsonify({"status": "error", "message": "Missing order_id or value"}), 400

  conn = mysql.connector.connect(**db_config)
  cursor = conn.cursor()
  cursor.execute("""
    INSERT INTO orders_log
    (order_id, order_value, currency, shipping, tax, discount, items_json, created_at, source)
    VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
  """, (
    order_id, value,
    data.get("currency", "EUR"),
    data.get("shipping", 0), data.get("tax", 0), data.get("discount", 0),
    json.dumps(data.get("items", []), ensure_ascii=False),
    datetime.utcnow(),
    "gtm-ss"
  ))
  conn.commit()
  cursor.close()
  conn.close()
  return jsonify({"status": "ok"}), 200
```

MySQL table schema:

```
orders_log
----------
id             AUTO_INCREMENT PRIMARY KEY
order_id       VARCHAR
order_value    DECIMAL
currency       VARCHAR
shipping       DECIMAL
tax            DECIMAL
discount       DECIMAL
items_json     JSON or TEXT
created_at     DATETIME
source         VARCHAR  -- e.g. "gtm-ss"
```

For a first implementation, storing items as JSON is enough. A normalized `order_items` table can come later.

## Apache reverse proxy and HTTPS

Server-Side GTM needs a public HTTPS URL to call, but Flask normally runs on `127.0.0.1:5000`. An Apache reverse proxy terminates SSL on a public host and forwards the request internally to Flask.

Enable the needed modules and configure a Virtual Host on port 443 with Let's Encrypt certificates, `ProxyPass` to the Flask instance, and `Header always set` directives for CORS if the Server-Side GTM domain differs from the API. With that in place, Server-Side GTM can POST purchases to `https://api.mysite.gr/gtm/purchase-log` — and every order drops into MySQL within milliseconds of the customer clicking “confirm”.

## FAQ

### Does this replace GA4?
No — GA4 still runs in parallel for attribution, audiences, and behavioural analytics. This pipeline adds an independent real-time revenue source that does not depend on GA4 being healthy at any given moment.

### What about privacy and GDPR compliance?
The MySQL log stores order-level data, not personal identifiers. Because we control the backend end-to-end, we can apply our own retention rules, access controls, and anonymisation logic — typically stricter than what third-party analytics provide.

### Can this run without Server-Side GTM?
Yes, you can POST directly from Web GTM to the Flask endpoint. But Server-Side GTM adds observability, debug tools, and the ability to enrich events with server-side data — worth it for production.
