Case Study · Process AutomationMarTech / CRM

40 hours of manual CRM reporting a month, replaced by one pipeline

A marketing team was running 500+ campaigns a month on WebEngage — a platform with no API for the content and reports they needed. We reverse-engineered the dashboard's own endpoints, intercepted its report emails, and built a scheduled pipeline that lands clean, categorized data in a warehouse. What took a skilled analyst two working days a week now runs unattended.

AM

Arham Mirkar

DataLayer — Enterprise Data Infrastructure

~40 hrs
Manual work removed / month
500+
Campaigns reported / month
4
Channels unified
0
Manual steps after trigger
The Problem

A reporting process that ran on a human

WebEngage is a powerful CRM, but it exposes no public API for campaign content or for the bulk stats export the team lived in. So every reporting cycle, someone repeated the same manual loop by hand — across email, push, WhatsApp, and web push:

  1. 1Log into the WebEngage dashboard and open the campaign stats report builder
  2. 2Request an “All Campaigns” export for the date range and wait for it to generate
  3. 3Watch an inbox for the report-ready email from noreply@webengage.com
  4. 4Click the tracking link, download the ZIP, and unzip the CSV
  5. 5Open each campaign one-by-one to read subject lines, push copy, and CTAs
  6. 6Hand-map every journey name to a business category in a spreadsheet
  7. 7Stitch it all into the weekly and monthly reports leadership expects

Documented at roughly 40–50 analyst hours a month across 500+ campaigns — slow, error-prone, and impossible to trust for month-over-month analysis.

The System

One trigger, seven stages, zero clicks

The pipeline reproduces every step a human took — then removes the human. The hardest part isn't any single call; it's that WebEngage hands the report back over email, so the system has to bridge an API, an inbox, and cloud storage without ever breaking.

Trigger

Scheduler

Windows Task Scheduler fires the pipeline weekly (Monday 09:00) and monthly (1st, 09:00). The same entrypoint is exposed to a CLI, a Flask endpoint, an optional n8n bridge, and the AI agent — one code path, four ways to start it.

Authenticate

Session cookies

WebEngage has no public content API, so the pipeline authenticates the way the dashboard does — with session cookies (WebKlipperAuth, _we_a_ssid, WeAuth, GCILB). Cookies are acquired four ways and stored encrypted.

Generate report

Dashboard API

A POST to the reverse-engineered /campaigns/stats/generate-report endpoint enqueues an “All Campaigns / DAILY” export server-side — the exact call the dashboard makes when a human clicks the button.

Intercept the email

IMAP side-channel

WebEngage delivers the finished report by email, not by API response. The pipeline polls an inbox over IMAP every 30s (up to 15 min), gated on freshness so it never grabs a stale report.

Unwrap the download

Base64 + GCS

The email only contains a click-tracking link. The pipeline decodes its base64 payload to recover the real Google Cloud Storage URL, then streams and unzips the CSV — no browser, no clicking.

Transform

Categorize + parse

Every journey is normalized and mapped to a business category via a 79-rule taxonomy. Email HTML is parsed for body copy and CTAs; push payloads are flattened across Android and iOS.

Load

Warehouse

Rows land in BigQuery with a delete-then-append window so re-runs never double-count, and campaign content is upserted into a Supabase repository that the whole stack can query.

Deep Dive 01

API archaeology

With no public API to call, the endpoints had to be recovered from the dashboard's own network traffic and turned into a private, stable SDK. Each call sends the browser-like headers and session cookies WebEngage expects, so the server can't tell the difference between the pipeline and a logged-in analyst.

MethodReverse-engineered endpointPurpose
POST/campaigns/stats/generate-reportEnqueue an All-Campaigns export for a date range
GET/emails/{id}?fetchAllVar=truePull full email content and every variation
GET/push-notifications/{id}Pull push copy, deep links, and payloads
GET/whatsapp-messages/{id}Pull WhatsApp template content
GET/journeys/{id}/conversion-statsPull journey-level conversion performance
GET/api/v2/accountsDiscover which projects a session can access

The exact payload that enqueues a report — recovered from the dashboard and issued directly:

POST /api/v2/accounts/{account}/campaigns/stats/generate-report

{
  "reportConfig": {
    "channel": "ALL",
    "from": "2026-06-01",
    "to": "2026-06-30",
    "reportFrequency": "DAILY",
    "splitByVariation": false
  }
}
Deep Dive 02

Auth that survives the real world

Session cookies expire, CAPTCHAs appear, and OTPs interrupt. Instead of one brittle login, the system has four ways to obtain a valid session — and stores every credential encrypted at rest.

1

Headless Playwright login

Scripted login with an OTP callback the operator submits from the UI — for fully automated, unattended runs.

2

Interactive Chrome profile

A persistent, headed Chrome session for when a CAPTCHA or OTP needs a human; cookies are harvested the moment the dashboard loads.

3

Chrome cookie auto-import

Reads the OS-encrypted Chrome cookie database directly and decrypts it with Windows DPAPI + AES-GCM — zero re-login.

4

Manual paste fallback

A paste box for headless or cloud environments where no browser is available. Every credential is encrypted at rest with Fernet.

A validity probe checks cookies before every run and silently re-authenticates when they lapse, then syncs the fresh session into the scheduled pipeline's config — so unattended runs keep working for weeks without anyone logging in.

Deep Dive 03

The email side-channel

WebEngage doesn't return the report in the API response — it emails a link when the export is ready. So the pipeline treats the inbox as part of the API: it polls over IMAP, verifies the message is fresh, then unwraps a click-tracking link to recover the real download URL.

Poll, with freshness gates

IMAP search for the report email every 30s for up to 15 minutes — rejecting anything older than the trigger so a stale report is never mistaken for the new one.

Decode the tracking link

The email only holds a click-tracking URL. Its base64 payload is padded, decoded, and parsed to JSON to recover the real Google Cloud Storage download link.

Stream, unzip, verify

The ZIP is streamed and extracted, with a guard that detects an HTML login page returned instead of a CSV — the tell-tale sign of an expired session.

# Recover the real download URL hidden inside the tracking link
p_value = unquote(p_value)
p_value += "=" * (4 - len(p_value) % 4) % 4      # fix base64 padding
payload = json.loads(base64.b64decode(p_value))
download_url = payload["toURL"]                   # → GCS report ZIP
Deep Dive 04

From raw CSV to trustworthy warehouse

Raw exports aren't analysis-ready. The transform layer turns messy CRM naming into clean BI dimensions, and the load layer is built so a re-run is always safe.

Journey taxonomy

A 79-rule mapping normalizes inconsistent journey names (spacing, casing, non-breaking spaces, even Arabic column headers) and resolves them to 14 business categories — the dimensions leadership actually reports on.

Abandoned CartWinBackSign-up VoucherView AbandonSearch Abandonment1st Purchase PushReviewDormant InstallsInternal CommsBest Price GuaranteedCross-SellReferralApp DownloadOrder Rejected

Idempotent loads

Every load deletes the target date window before appending, so re-running a report can never double-count a day. Campaign content is upserted into Supabase and read back in batches to keep syncs fast at scale.

DELETE FROM report
WHERE Day BETWEEN :min AND :max;
-- then append the fresh window
to_gbq(df, table, if_exists="append")
2,257
Campaigns in a single month's export
9,230
Stat rows synced in one run
79 → 14
Naming rules to clean categories
BigQuery
+ Supabase content repository
Beyond the Pipeline

From a script to a platform

The automation grew into an internal product — an ops console the team runs day-to-day, a searchable content repository, campaign drafting from a spreadsheet, and an AI assistant that queries the whole warehouse in plain English.

Flask UI

CampaignPilot ops console

A local web console that manages the whole session lifecycle — login, project discovery, cookie testing and refresh — and runs the full multi-channel export with live job status, stop, and skip controls.

Supabase

Content repository

Every campaign’s copy and stats are cached in Postgres with batched reads and upserts, turning years of scattered campaigns into one searchable, deduplicated source of truth.

Reverse-engineered writes

Draft campaign creation

A state machine that creates push and web-push campaigns from an Excel schedule — create → variations → schedule → conversions → verify — deliberately left in DRAFT for human QA before launch.

LLM agent

ReAct AI assistant

A tool-using agent over the warehouse: it can search campaigns, surface top performers, pull journey detail, trigger the report pipeline, and run sandboxed pandas analysis on demand.

Under the Hood

Technology

LayerTechnologies
AutomationPython, requests, Playwright, BeautifulSoup, concurrent.futures
Auth & SecuritySession-cookie auth, Windows DPAPI + AES-GCM decrypt, Fernet at-rest encryption
Email side-channelimaplib (IMAP SSL), base64 tracking-URL decode, streamed GCS ZIP download
Transformpandas, openpyxl, 79-rule journey taxonomy, Unicode/Arabic header normalization
WarehouseGoogle BigQuery (pandas-gbq), Supabase / PostgREST content + stats repo
OrchestrationWindows Task Scheduler, Flask ops console, optional n8n bridge, ReAct LLM agent
AM

Arham Mirkar

Founder & Data Engineer, DataLayer

“This one started as a favor to a marketing team drowning in manual exports. The fun part wasn't any single API call — it was making an inbox, a dashboard, and a cloud bucket behave like one reliable system that runs while everyone sleeps. I designed and built the whole thing myself, end to end.”

Is a person your team's reporting bottleneck?

If a workflow has clear rules and eats hours every week, it can almost certainly run unattended. That's what we build.