惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
大猫的无限游戏
大猫的无限游戏
博客园 - 聂微东
Jina AI
Jina AI
The Cloudflare Blog
V
Visual Studio Blog
博客园_首页
量子位
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 【当耐特】
爱范儿
爱范儿
博客园 - 三生石上(FineUI控件)
小众软件
小众软件
博客园 - 司徒正美
阮一峰的网络日志
阮一峰的网络日志
Last Week in AI
Last Week in AI
V
V2EX
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 叶小钗
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
WordPress大学
WordPress大学
宝玉的分享
宝玉的分享
T
Tailwind CSS Blog
博客园 - Franky

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
How we connected off-page content to actual revenue with ...
Priyom Sarkar · 2026-06-25 · via DEV Community

Priyom Sarkar

For a long time our demo bookings showed up in the CRM with a source of (direct). Someone read a Reddit answer, clicked through, booked a call, and as far as our data was concerned, fell out of the sky. We knew the off-page content was working. We could not prove which piece.

This is the writeup of the small thing we built to fix that. It is not clever. That is the point. Attribution does not need to be clever; it needs to actually fire on every booking.

The problem in one sentence

Calendly bookings landed in HubSpot with no campaign data, because the UTM parameters on the landing URL were not being carried through into the contact record. So every off-page channel collapsed into (direct), and we could not tell a Reddit-sourced demo from a Medium-sourced one from a podcast-sourced one.

The shape of the fix

Three pieces.

  1. A strict UTM convention so every off-domain link is tagged consistently before it ever ships.
  2. A Calendly webhook that fires on invitee.created.
  3. A small handler that reads the UTMs off the booking and writes them onto the HubSpot contact's self_reported_sourcestandard UTM properties.

1. The UTM convention

The unglamorous part that makes the rest work. Every link we publish off-domain follows the same pattern:

https://asanify.com/<path>/?utm_source=<platform>&utm_medium=<type>&utm_campaign=<topic>_<yyyymm>&utm_content=<placement>

utm_source is the platform token, lowercase, single word: reddit, quora, medium, devto, linkedin. Crucially it is never website or our own brand name, because those collapse straight back to direct. We enforce this with a regex check in CI before any draft is allowed to ship:

import re

def verify_utms(text: str) -> list[str]:
    fails = []
    urls = re.findall(
        r'https?://(?:[a-z0-9-]+\.)*(?:asanify\.com|calendly\.com)[^\s\)\]"]*',
        text,
    )
    for url in urls:
        for p in ("utm_source=", "utm_medium=", "utm_campaign="):
            if p not in url:
                fails.append(f"missing {p}: {url}")
        if re.search(r"utm_source=(website|na|none|test)\b", url, re.I):
            fails.append(f"bad source value: {url}")
    return fails

If verify_utms returns anything, the draft does not publish. A missing UTM is treated as a defect, not a nice-to-have. We standardized the whole thing on a single conventions doc that every content producer reads, which is the boring governance move that actually held the data clean. The corridor pages these links point at live on our EOR hub.

2. The Calendly webhook

Calendly fires a webhook on invitee.created. The payload includes tracking, which carries through the UTM parameters present on the scheduling link. That is the whole trick: Calendly already captures them; you just have to read them and forward them.

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.post("/marketing/calendly")
def calendly_hook():
    event = request.get_json(force=True)
    if event.get("event") != "invitee.created":
        return jsonify(ok=True), 200

    payload = event["payload"]
    email = payload["email"]
    tracking = payload.get("tracking", {}) or {}

    utms = {
        "utm_source":   tracking.get("utm_source"),
        "utm_medium":   tracking.get("utm_medium"),
        "utm_campaign": tracking.get("utm_campaign"),
        "utm_content":  tracking.get("utm_content"),
    }
    upsert_hubspot_contact(email, utms)
    return jsonify(ok=True), 200

3. Writing it onto the HubSpot contact

The handler upserts the contact and stamps the UTMs, plus a human-readable self_reported_source the sales team can filter on without learning UTM syntax.

import requests, os

HS = "https://api.hubapi.com/crm/v3/objects/contacts"
HEADERS = {"Authorization": f"Bearer {os.environ['HUBSPOT_TOKEN']}"}

def upsert_hubspot_contact(email: str, utms: dict):
    source = utms.get("utm_source") or "direct"
    props = {
        "email": email,
        "self_reported_source": source,
        "utm_source": source,
        "utm_medium": utms.get("utm_medium") or "",
        "utm_campaign": utms.get("utm_campaign") or "",
    }
    # idempotent upsert by email
    requests.post(
        f"{HS}?idempotencyKey={email}",
        json={"properties": props},
        headers=HEADERS,
        timeout=10,
    )

A couple of things we learned the hard way. Make the webhook idempotent, because Calendly will retry on a slow response and you do not want duplicate writes. Return 200 fast and do the slow CRM work behind a queue if your handler is heavy, otherwise the retries pile up. And log the raw tracking blob for a while, because the first week we found a chunk of links in the wild that predated the convention and arrived with no UTMs at all, which told us exactly which old content to go back and re-tag.

What it changed

Once this was live, the source breakdown on demo bookings stopped being a mystery. We could finally see which off-page channel was actually producing booked calls instead of just traffic, and we reallocated effort accordingly. The number that surprised us most was how much of our demand was coming from AI assistants citing our content, which only became visible because the source value was specific instead of a generic bucket.

We write more about how that AI-citation channel works for us on our EOR hub.
Nothing here is hard engineering.

The leverage is entirely in two things: a UTM convention you actually enforce, and a webhook that fires on every booking with no exceptions. Get those two right and your "where do demos come from" question has a real answer.