The Business Challenge

Mobile attribution platforms (AppsFlyer, Adjust) and ad platforms (Google Ads, Meta, TikTok) natively optimize on the earliest digital signal: the app install or the lead form submission. However, for a fintech lending app, an install or even an application is a low-fidelity signal. The true business value is realized only when a loan is disbursed, which typically happens 3 to 14 days later after credit checks and underwriting. Without feeding this offline conversion data back to the ad algorithms, Smart Bidding optimizes for volume of applications rather than quality. With it, we shift the bidding strategy to target actual loan disbursements and pass the real monetary value. The historical difference in Return on Ad Spend (ROAS) is typically 2–4×. S2S Pipeline Architecture

Solution Architecture

To solve this we designed a Server-to-Server (S2S) pipeline. Our core design principle was "one normalization step, three adapters." Instead of building three disparate API connections that extract data from the CRM independently, we built a centralized staging table.

CRM (loan events)
↓ webhook / nightly export
PostgreSQL (Central Staging)
↓ Airflow DAG (daily, 02:00 UTC+5)
├── 1. Google Ads Offline Conversions API
├── 2. Meta Conversions API (CAPI)
└── 3. TikTok Events API

Step-by-Step Implementation

1. Click ID Capture (Client-Side)

Click IDs must be captured at the exact moment of the ad click and stored server-side. Relying solely on client-side storage (cookies, localStorage) leads to data loss for users who switch devices or use browsers with stringent Intelligent Tracking Prevention (ITP) like Safari.

(function() {
var params = new URLSearchParams(window.location.search);
var ids = {
gclid: params.get('gclid'),
fbclid: params.get('fbclid'),
ttclid: params.get('ttclid'),
ts: Date.now()
};
Object.keys(ids).forEach(function(k) {
if (ids[k]) document.cookie = k + '=' + ids[k] + ';max-age=2592000;path=/;SameSite=Lax';
});
if (ids.gclid || ids.fbclid || ids.ttclid) {
navigator.sendBeacon('/api/track-click', JSON.stringify(ids));
}
})();

2. Centralized Staging Schema (PostgreSQL)

We built a normalized schema where each row represents a conversion with tracking tokens and boolean flags indicating sync status per network.

CREATE TABLE offline_conversions (
id SERIAL PRIMARY KEY,
loan_id VARCHAR(64) NOT NULL,
iin VARCHAR(12),
phone_e164 VARCHAR(20),
gclid VARCHAR(256),
fbclid VARCHAR(256),
ttclid VARCHAR(256),
click_ts TIMESTAMPTZ,
conversion_ts TIMESTAMPTZ NOT NULL,
conversion_type VARCHAR(64) NOT NULL,
loan_amount_kzt NUMERIC(14,2),
currency CHAR(3) DEFAULT 'KZT',
uploaded_google BOOLEAN DEFAULT FALSE,
uploaded_meta BOOLEAN DEFAULT FALSE,
uploaded_tiktok BOOLEAN DEFAULT FALSE,
upload_error TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);

3. IIN as the Ultimate Match Key

In Kazakhstan, the IIN (ИИН, 12-digit national ID) represents an exceptionally strong matching key. Every adult possesses one, it remains completely stable, and it is universally collected during loan applications. By hashing the IIN, we process it as a highly reliable external_id for Meta CAPI and TikTok Events API.

import hashlib
def hash_iin(iin: str) -> str:
normalized = iin.strip().lower()
return hashlib.sha256(normalized.encode()).hexdigest()

4. Airflow Orchestration

We orchestrated the actual synchronization using Apache Airflow. The DAG pulls fresh conversions nightly and fans out the upload parallelly to the three respective APIs. Deduplication is heavily enforced using the loan_id.

Results & Impact

Implementing the S2S pipeline fundamentally shifted our UA strategy. We bypassed the limitations of iOS 14.5+ and ITP by owning the tracking server-side. Within 30 days of algorithm relearning, we recorded:

  • A 38% decrease in Cost Per Funded Loan.
  • 100% accurate ROAS reporting in ad managers.
  • Elimination of manual CSV uploads, saving the analytics team over 10 hours a week.

Further Reading & Deeper Dive

For a broader perspective on Server-Side Tagging and the inevitable shift away from client-side cookies, check out: