Prooflytics + DV360: Attribution of Programmatic Ads to Closed B2B Deals

Prooflytics + DV360: Attribution of Programmatic Ads to Closed B2B Deals

Display & Video 360 (DV360) is Google’s enterprise platform for programmatic media buying: display, video, Connected TV, audio, and native. Enterprise B2B companies use DV360 for ABM (Account-Based Marketing), CRM list retargeting, and brand awareness campaigns reaching target audiences. Standard DV360 attribution works via view-through and click-through conversions tracked through the Floodlight pixel in Campaign Manager 360. The problem: Floodlight records a conversion at the site level (form submission, page visit), but has no way of knowing whether that form submission became a closed deal 60 days later.

Prooflytics connects DV360 click and impression data to the CRM lifecycle. When a deal is marked Closed Won, it sends an offline conversion to Campaign Manager 360 - and DV360 incorporates it into algorithm optimization.

Floodlight Activity - a Campaign Manager 360 tag (pixel) for tracking conversions. DV360 uses Floodlight data for bidding optimization. DCLID (DoubleClick Click ID) - a unique click identifier from DV360/CM360, the equivalent of gclid for Google Ads.

Attribution Architecture

User sees / clicks a DV360 ad
  -> DCLID is appended to the URL: yoursite.com/demo?dclid=AKEYu...
  -> Your Floodlight pixel fires on page visit
  -> JS captures dclid from the URL and stores it in cookie/localStorage

Lead form submission
  -> dclid passed as a hidden field
  -> CRM: deal created, dclid stored in a custom field

Prooflytics
  -> On Closed Won: Campaign Manager 360 Offline Conversions API
  -> Upload: dclid + timestamp + value
  -> DV360 incorporates into algorithm optimization

Capturing DCLID on Your Site

(function() {
    var params = new URLSearchParams(window.location.search);
    var dclid  = params.get("dclid");
    if (dclid) {
        localStorage.setItem("dclid", dclid);
        document.cookie = "dclid=" + dclid + ";path=/;max-age=2592000";  // 30 days
    }
    // Also capture gbraid/wbraid for iOS (Google Ads)
    ["gbraid", "wbraid"].forEach(function(p) {
        var val = params.get(p);
        if (val) localStorage.setItem(p, val);
    });
})();

function getCapturedId() {
    return (
        new URLSearchParams(window.location.search).get("dclid") ||
        localStorage.getItem("dclid") ||
        getCookie("dclid") || ""
    );
}

function getCookie(name) {
    var m = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)"));
    return m ? m[2] : "";
}

Uploading Offline Conversions to Campaign Manager 360

import requests, os, time, hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)

# CM360 OAuth2 credentials (service account)
CM360_SERVICE_ACCOUNT_JSON = os.environ.get("CM360_SERVICE_ACCOUNT_JSON", "")
CM360_PROFILE_ID           = os.environ["CM360_PROFILE_ID"]
CM360_FLOODLIGHT_CONFIG_ID = os.environ["CM360_FLOODLIGHT_CONFIG_ID"]
CM360_ACTIVITY_ID          = os.environ["CM360_FLOODLIGHT_ACTIVITY_ID"]  # "deal_won"

KOMMO_SUBDOMAIN = os.environ["KOMMO_SUBDOMAIN"]
KOMMO_TOKEN     = os.environ["KOMMO_ACCESS_TOKEN"]
CLOSED_WON_ID   = int(os.environ["KOMMO_CLOSED_WON_ID"])
KOMMO_CF_DCLID  = int(os.environ["KOMMO_CF_DCLID_ID"])

KOMMO_BASE = f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4"
KOMMO_HDR  = {"Authorization": f"Bearer {KOMMO_TOKEN}", "Content-Type": "application/json"}

def get_cm360_access_token() -> str:
    import json as json_mod
    from google.oauth2 import service_account
    from google.auth.transport.requests import Request as GRequest
    sa_info  = json_mod.loads(CM360_SERVICE_ACCOUNT_JSON)
    creds    = service_account.Credentials.from_service_account_info(
        sa_info,
        scopes=["https://www.googleapis.com/auth/dfatrafficking"],
    )
    creds.refresh(GRequest())
    return creds.token

def get_lead_dclid_email(lead_id: int) -> tuple[str, str, float]:
    r = requests.get(
        f"{KOMMO_BASE}/leads/{lead_id}",
        headers=KOMMO_HDR,
        params={"with": "contacts,custom_fields_values"},
    )
    lead  = r.json()
    dclid = ""
    for cf in lead.get("custom_fields_values", []) or []:
        if cf.get("field_id") == KOMMO_CF_DCLID:
            vals = cf.get("values", [])
            if vals:
                dclid = vals[0].get("value", "")
                break

    email = ""
    contacts = lead.get("_embedded", {}).get("contacts", [])
    if contacts:
        rc = requests.get(
            f"{KOMMO_BASE}/contacts/{contacts[0]['id']}",
            headers=KOMMO_HDR,
            params={"with": "custom_fields_values"},
        )
        for cf in rc.json().get("custom_fields_values", []) or []:
            if cf.get("field_code") == "EMAIL":
                vals = cf.get("values", [])
                if vals:
                    email = vals[0].get("value", "")
                    break

    revenue = float(lead.get("price") or 0)
    return dclid, email, revenue

def sha256_email(email: str) -> str:
    return hashlib.sha256(email.strip().lower().encode()).hexdigest()

def upload_offline_conversion(dclid: str, email: str, revenue: float):
    token = get_cm360_access_token()
    hdrs  = {
        "Authorization": f"Bearer {token}",
        "Content-Type":  "application/json",
    }
    timestamp_micros = int(time.time() * 1_000_000)

    # CM360 Offline Conversion
    body = {
        "kind":                    "dfareporting#conversionsBatchInsertRequest",
        "conversions": [{
            "dclid":                  dclid,
            "floodlightActivityId":   CM360_ACTIVITY_ID,
            "floodlightConfigurationId": CM360_FLOODLIGHT_CONFIG_ID,
            "ordinal":                str(timestamp_micros),
            "timestampMicros":        timestamp_micros,
            "value":                  revenue,
            "quantity":               1,
            "encryptedUserId":        sha256_email(email) if email else None,
        }],
    }
    r = requests.post(
        f"https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/{CM360_PROFILE_ID}/conversions/batchinsert",
        headers=hdrs,
        json=body,
    )
    return r.status_code, r.text

def add_note(lead_id: int, text: str):
    requests.post(
        f"{KOMMO_BASE}/notes",
        headers=KOMMO_HDR,
        json=[{
            "entity_id":   lead_id,
            "entity_type": "leads",
            "note_type":   "common",
            "params":      {"text": text},
        }],
    )

@app.route("/webhooks/kommo", methods=["POST"])
def kommo_webhook():
    data = request.json or {}
    for lead_data in data.get("leads", {}).get("status", []):
        lead_id    = lead_data.get("id")
        new_status = lead_data.get("status_id")
        if new_status != CLOSED_WON_ID:
            continue

        dclid, email, revenue = get_lead_dclid_email(lead_id)
        if not dclid:
            continue  # not a DV360 source

        status_code, resp = upload_offline_conversion(dclid, email, revenue)
        add_note(
            lead_id,
            f"CM360 Offline Conversion: uploaded. DCLID: {dclid[:20]}... "
            f"Revenue: ${revenue:.2f}. Status: {status_code}",
        )

    return jsonify({"status": "ok"}), 200

Configuring a Floodlight Activity in CM360

Campaign Manager 360 -> Floodlight Activities -> New Activity:

  • Type: Custom
  • Tag string: deal_won (this becomes the floodlightActivityId)
  • Counting method: Unique

After creating it, record the Activity ID and Floodlight Configuration ID in your environment variables.

DV360 and Optimization on Offline Conversions

Once offline conversions are uploaded to CM360, DV360 automatically factors them into bid optimization. In DV360: Line Item -> Optimization -> select the Floodlight Activity “deal_won”. The algorithm will then optimize toward actual closed deals rather than form submissions.

This changes the economics of programmatic: instead of optimizing for CTR or form conversions, DV360 will target audiences that statistically convert into closed deals more often.

The Role of Prooflytics

Prooflytics automates this pipeline across multiple channels simultaneously - uploading offline conversions to CM360/DV360, Google Ads (gclid), Meta (fbclid), and LinkedIn (li_fat_id) on every Closed Won event. No need to manually configure a separate endpoint for each channel.

The example above covers a single-channel DV360 implementation. For more on multi-channel attribution with Prooflytics, see the article Prooflytics + Amazon DSP.

Who This Is For

Enterprise B2B companies running DV360 campaigns: ABM display against target account lists, programmatic video for brand awareness. Especially relevant when the DV360 budget exceeds $20k/month and standard metrics cannot measure the real ROI of the channel.

Frequently Asked Questions

Is CM360 required separately, or is DV360 enough?

DV360 uses CM360 as its measurement layer. Offline conversions are uploaded through the CM360 API, not the DV360 API. CM360 (Campaign Manager) is free for DV360 customers and is configured through Google Marketing Platform.

How long does DV360 retain a DCLID for attribution?

A DCLID is valid for attribution for 30 days from the click (the default attribution window). For B2B with a long sales cycle (90+ days), you need to extend the attribution window in CM360: Floodlight Configuration -> Attribution -> increase the click-through window to 90 days. Store the DCLID in your CRM indefinitely - even if CM360 does not credit the attribution, the data is still valuable for internal analytics.

What permissions does the service account need for the CM360 API?

Add the Service Account in Google Cloud to CM360 as a User (User Profile) with the role Traffic Manager or higher. Required scope: https://www.googleapis.com/auth/dfatrafficking. The “Reporting” role is insufficient - Traffic Manager is required to write conversions.

Summary

Prooflytics + DV360 - offline conversion attribution for programmatic:

  • Capture DCLID from URL -> localStorage + cookie -> CRM custom field
  • On Closed Won: CM360 Offline Conversions API batchinsert with DCLID + revenue
  • Service Account OAuth2 for CM360 (google-auth library)
  • DV360 optimizes bids toward the deal_won Floodlight Activity
  • Extend the attribution window to 90 days for long B2B sales cycles

If you need help setting up DV360 offline conversions and Prooflytics attribution, reach out to the Exceltic.dev team.

More articles

All →