Tracked Links & Analytics
The analytics module answers “which channel actually converted?” — a facebook group, a newsletter, a specific post — by minting a per-source tracked link, redirecting the visitor through it, and rolling up visits vs. paid conversions per source.
The funnel has three hops:
tracked link (?s=<source>) → your landing page (?s= forwarded) → paywall (e.g. HelloAsso)1. Mint a tracked link
Section titled “1. Mint a tracked link”From /analytics in the dashboard, or via the SDK:
import { PlatformApiClient } from '@viite-ai/platform-sdk';
const client = new PlatformApiClient({ baseUrl, sessionToken });const link = await client.analytics.createLink({ label: 'Summer Fest', targetUrl: 'https://your-event-site.example.com', // your landing page, NOT the paywall directly});targetUrl should be your landing page — the page a social-media click should land on — not the
payment link itself. The landing page is the one that has the “book now” / “ticketing” call to action
linking onward to the paywall.
2. Share a per-source URL
Section titled “2. Share a per-source URL”Each channel gets its own share URL built from the link’s slug:
https://<api host>/api/analytics/l/<slug>?s=fb-groupAs (or the utm_source alias) is whatever label you want for that channel — a facebook group name,
newsletter, etc. Every hit records a source-attributed visit, then 302-redirects to targetUrl
with the same ?s= appended — the landing page receives the exact source the visitor arrived
with.
Why a query param and not the Referer header? Referer is unreliable across a landing page we
don’t run infra for: many in-app browsers (Facebook’s especially) strip or rewrite it, it’s governed
by whatever Referrer-Policy the landing page’s own host sets, and it’s inconsistent across a
redirect chain. A query param is explicit and works on any static host.
3. Forward the source from your landing page
Section titled “3. Forward the source from your landing page”Your landing page needs to read ?s= (or utm_source) from its own URL and pass it along to its
outbound paywall link, so the attribution survives that second hop too. A minimal vanilla-JS snippet,
dropped once into your page layout, covers every outbound link to a fixed paywall URL:
<script> const params = new URLSearchParams(location.search); const source = params.get('s') || params.get('utm_source'); if (source) { document.querySelectorAll('a[href^="https://www.helloasso.com"]').forEach((a) => { const url = new URL(a.href); url.searchParams.set('s', source); a.href = url.toString(); }); }</script>Adjust the selector to match your own paywall’s URL prefix.
4. Report a conversion
Section titled “4. Report a conversion”The redirect appends two params to your landing page URL: ?s=<source> (the traffic source) and
?lk=<slug> (which tracked link the visitor came through). When someone converts — signs up, submits
a form, whatever counts for you — post both back:
<script> const params = new URLSearchParams(location.search); const linkSlug = params.get('lk'); if (linkSlug) { fetch('https://platform-api.viite.ai/api/analytics/beacon/signup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ linkSlug, source: params.get('s'), // Optional: any stable id for this conversion. Makes a retry idempotent // across processes, which the 30-second in-memory dedupe window is not. externalId: newAccountId, }), }); }</script>The beacon needs no authentication and always answers 202 — it sits in front of your signup flow, so
a failure on our side must never surface as an error in yours. It records the conversion against the
org that owns the link, resolved from the slug: there is no way to name an org in the request, so
nobody can write into your funnel by guessing an id.
Same guards as the redirect: crawler user-agents are ignored, and repeats from the same visitor inside 30 seconds count once.
5. Read the report
Section titled “5. Read the report”/analytics shows a per-source table — visits, signups, signup rate, paid, conversion, revenue — for a
date range and optional link filter (both URL-addressable, so a shared link reproduces the exact view):
const report = await client.analytics.report({ from, to, slug });// report.sources: Array<{ source, reachedPaywall, signedUp, paid, signupRate, paidRate, revenueCents }>Rank channels on signupRate, not on visits. A channel can deliver most of your traffic and none
of your accounts; the visit count alone can’t tell you which one you’re looking at.
Claude can read this too, if you’ve added the Claude connector —
create_tracked_link mints the links and get_funnel_report reads the funnel back.
Known limitation: HelloAsso paid events aren’t source-attributed yet
Section titled “Known limitation: HelloAsso paid events aren’t source-attributed yet”Register your org’s HelloAsso webhook (/api/analytics/webhooks/helloasso/:orgId, shown on
/analytics) so completed payments record a paid event. Today that only works with a plain
HelloAsso payment link — HelloAsso’s webhook payload doesn’t echo back arbitrary query params, so
paid events land in an unattributed bucket regardless of step 3 above. Source-attributed paid
requires switching to HelloAsso’s Checkout-Intent API (which sets metadata.source explicitly, and
is echoed back) — a larger, separate integration. Until then, this funnel gives you accurate
visits per source, signups per source (via the beacon in step 4), and landing-page
click-through; only paid-per-source stays aggregate-only. The signup stage exists precisely
because of this gap: it is the first conversion that can be attributed, since the source travels
with the visitor rather than arriving in a third party’s webhook.
