The Context

In an omnichannel retail banking environment, the customer journey is rarely confined to a digital ecosystem. Online-to-Offline (O2O) attribution solves the critical gap of tracing a digital ad click to a branch visit where a financial instrument is physically issued (e.g., car loan, physical credit card). The core technical challenge: the click happens on a mobile device carrying network tracking parameters, but the conversion is logged by a credit officer in a legacy CRM devoid of digital context. O2O Funnel Matching

System Components

We built a closed-loop tracking mechanism with four pillars:

1. JS Click Tracker — Caputres UTM + ad network IDs continuously.
2. Backend Storage — Stores click sessions in a high-throughput PostgreSQL DB.
3. CRM Integration — A reconciliation engine matching terminal events to sessions.
4. Airflow DAG — The outbound loop sending matches back to networks.

The Breakthrough: session_id Persistence

The traditional approach involves retroactive matching by PII (e.g. Phone number). This causes a 15–30% data loss due to multi-device browsing. Our highest-fidelity matching strategy involved injecting an immutable session_id into the very fabric of the application funnel.

  1. Trigger: User lands with a gclid. The JS tracker fires and mints a unique session_id cookie.
  2. Persistence: When the user opens the application form React component, a hidden field injects this session_id.
  3. Submission: The form payload hitting the backend carries the session_id alongside the application data.
  4. Resolution: The CRM records the business conversion directly coupled with the session_id. Lookup is O(1) yielding a strictly 100% deterministic match rate.

Postgre / CRM Matching Logic

When a session_id is somehow dropped (e.g., cross-device organic return), the system gracefully cascades to a secondary PII-probabilistic match prioritizing IIN and recency.

def match_conversion_to_click(conversion: dict):
# 1. Deterministic session_id match (Highest priority)
if conversion.get('session_id'):
row = db.fetchone(
'SELECT session_id FROM click_sessions WHERE session_id = %s',
[conversion['session_id']]
)
if row: return row['session_id']
# 2. Probabilistic IIN + Recency match (Fallback)
if conversion.get('iin'):
row = db.fetchone("""
SELECT cs.session_id FROM click_sessions cs
JOIN application_sessions aps ON aps.session_id = cs.session_id
WHERE aps.iin = %s
AND cs.created_at >= %s - INTERVAL '90 days'
AND (cs.gclid IS NOT NULL OR cs.fbclid IS NOT NULL)
ORDER BY cs.created_at DESC LIMIT 1
""", [conversion['iin'], conversion['converted_at']])
if row: return row['session_id']
return None

Impact

This multi-tiered O2O pipeline eliminated the "black box" of physical branch conversions. For the first time, marketing could attribute offline revenue directly to individual Meta and Google campaigns. It allowed the reallocation of roughly $500K in annual budget from underperforming top-of-funnel campaigns directly into high-converting physical delivery networks.

Further Reading & Deeper Dive

Connecting online intent to physical conversions is challenging globally, not just in Kazakhstan.