Web SDK Integration
The ChottuLink Web SDK brings attribution, identity mapping, and event tracking to your website — unifying your mobile and web funnels in one dashboard. It:
- Creates a stable per-browser device ID and attributes web visits back to the ChottuLink short-link click that brought the visitor
- Maps anonymous browser visitors to your own customer IDs (
identify()) - Tracks signups, conversions (with revenue), and custom events
- Works as an npm package (React, Vue, Next.js, etc.) or a CDN
<script>tag, with a small footprint and signal-gated initialization to minimize backend traffic
The Attribution & Analytics page covers install attribution for mobile apps (organic vs. attributed installs). The Web SDK instead attributes browser visits — UTM parameters, ad-click IDs, and ChottuLink link clicks that land a visitor on your site. Both funnels roll up into the same ChottuLink dashboard.
Prerequisites
- A ChottuLink account with your public API key (Dashboard → Settings → API Keys)
- Your website's domain added to Allowed Web Origins in the ChottuLink Dashboard — requests from unregistered origins are rejected with a CORS error
Installation
- npm / bundler
- Script Tag (CDN)
npm install @chottulink/web-sdk
import ChottuLink from '@chottulink/web-sdk';
await ChottuLink.init({ apiKey: 'YOUR_PUBLIC_API_KEY' });
Add this to the <head> or bottom of the <body> of your website:
<!-- 1. Optional: stub so you can call SDK methods before the script loads -->
<script>
window.ChottuLink = window.ChottuLink || function() {
(window.ChottuLink.q = window.ChottuLink.q || []).push(arguments);
};
</script>
<!-- 2. Queue your init call -->
<script>
ChottuLink('init', { apiKey: 'YOUR_PUBLIC_API_KEY' });
</script>
<!-- 3. Load the SDK async — it replays the queue on boot -->
<script
src="https://chottulink.com/downloads/sdks/web/v1.0.10/chottulink.iife.js"
async
crossorigin="anonymous">
</script>
The stub in step 1 lets you call ChottuLink(...) before the script has finished loading — no events are lost.
For an extra integrity check, add the integrity attribute to the script tag:
<script
src="https://chottulink.com/downloads/sdks/web/v1.0.10/chottulink.iife.js"
integrity="sha384-LTbMxZbtAQXF48yCk0NAXUVvF4HHg7dymHgIQlRXHqBR8iRN8R+P6YyF7Cdha7b3"
crossorigin="anonymous">
</script>
Initialize the SDK
Call init() once, as early as possible on every page load.
await ChottuLink.init({
apiKey: string, // Required: your organization public API key
baseUrl?: string, // Optional: custom backend URL (e.g. a first-party proxy)
cookieDomain?: string, // Optional: root domain for the device ID cookie (default: auto-detected apex domain)
autoPageTracking?: boolean, // Optional: auto-track SPA route changes (default: true)
allowIframe?: boolean, // Optional: allow tracking inside iframes (default: false)
consentMode?: 'required' | 'not_required', // Optional: buffer until consent (default: 'not_required')
debug?: boolean // Optional: enable console logging
});
init() returns a Promise<void>. You can await it or fire-and-forget — any identify() / track*() calls made before it resolves are queued and flushed automatically.
To limit unnecessary network requests, the SDK only calls the backend on page load if the device hasn't been seen before (first-ever visit) or the URL carries an attribution signal (cl_link_id, a UTM parameter, or an ad-click ID like gclid/fbclid). Plain internal navigation without a new signal doesn't trigger another attribution call.
Tracking Events
identify()
Maps the current browser to a known user in your system. State persists for the tab session (sessionStorage) and is attached to subsequent track calls automatically — you don't need to re-pass customer_id to every call.
ChottuLink.identify({
customer_id: string, // Required: your internal user ID
email?: string, // Hashed before sending
phone?: string, // Hashed before sending
name?: string,
email_sha256?: string, // Pre-hashed email, if you prefer not to send raw PII
phone_sha256?: string // Pre-hashed phone
});
trackLead()
Call when a user completes a signup or registration form.
await ChottuLink.trackLead({
customer_id: 'user_123',
email: 'alice@example.com',
name: 'Alice',
event_id?: string, // Optional idempotency key
client_timestamp?: string, // Optional ISO-8601 timestamp
});
All fields are optional — call it with no arguments and the SDK still sends device and attribution context.
trackConversion()
Call when a payment or high-value event completes. revenue and currency are required.
await ChottuLink.trackConversion({
revenue: 49.99, // Required: amount in major units, not cents
currency: 'USD', // Required: ISO-4217 code
transaction_id: 'txn_abc', // Recommended: used for dedup on retries
customer_id: 'user_123',
product_id: 'plan_pro',
});
trackEvent()
Track any other event.
await ChottuLink.trackEvent('add_to_cart', {
product_id: 'SKU_123',
metadata: { section: 'hero', variant: 'B' },
});
If your checkout flow can fire the same conversion twice (double-click, retry), send the same transaction_id on both calls — the backend dedupes so you don't double count.
Reading Attribution
const attr = ChottuLink.getAttribution();
// Returns null if called before init resolves, or on organic traffic
if (attr) {
console.log(attr.cl_link_id, attr.utm_source, attr.utm_campaign);
}
AttributionContext shape:
interface AttributionContext {
cl_link_id: string | null;
utm_source: string | null;
utm_medium: string | null;
utm_campaign: string | null;
utm_term: string | null;
utm_content: string | null;
custom_data: Record<string, unknown> | null;
}
The SDK automatically captures these URL parameters on landing:
| Parameter | Source |
|---|---|
cl_link_id | ChottuLink deep link |
utm_source, utm_medium, utm_campaign, utm_term, utm_content | UTM tags |
fbclid | Facebook / Meta Ads |
gclid, gbraid, wbraid | Google Ads |
ttclid | TikTok Ads |
Consent Mode (GDPR)
If your market requires consent before tracking, set consentMode: 'required' — all cookies and API calls are buffered until you call grantConsent().
await ChottuLink.init({
apiKey: 'YOUR_PUBLIC_API_KEY',
consentMode: 'required',
});
// On banner accept:
ChottuLink.grantConsent(); // flushes buffered calls in order
// On banner decline:
ChottuLink.denyConsent(); // discards all buffered calls silently
// Check current state:
ChottuLink.hasConsent(); // boolean
SPA Navigation & Attribution Re-checks
autoPageTracking: true (the default) patches history.pushState, history.replaceState, and popstate so client-side route changes are re-checked for attribution signals — the same signal-gated check that runs on the initial page load (see the note under Initialize the SDK).
Despite the name, this doesn't send a "page view" event — it just re-checks the new URL for attribution signals, so a UTM-tagged link clicked inside your app (e.g. a promo banner doing pushState) still gets captured even without a full page reload. No signal in the URL means no network call.
If you'd rather not have the SDK monkey-patch history globally (for example, if your router already does its own patching and you want to avoid double-patching), set autoPageTracking: false and call trackPageView() yourself from your router's navigation handler instead:
ChottuLink.trackPageView();
Don't leave autoPageTracking: true and also call trackPageView() manually — the checks are redundant (though not harmful, since same-URL calls within a session are deduped internally). Pick one.
First-Party Proxy (Ad Blocker Resilience)
If ad blockers are a concern, proxy SDK requests through your own domain using the baseUrl option:
await ChottuLink.init({
apiKey: 'YOUR_PUBLIC_API_KEY',
baseUrl: 'https://yoursite.com/cl-proxy',
});
Configure your proxy to forward POST /cl-proxy/* → https://api.chottulink.com/*, forwarding all headers except Host.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No attribution recorded | Page loaded without attribution params in the URL | Check the original landing URL; getAttribution() returns null on organic traffic |
getAttribution() always null | Called before init() resolves | await init() first, or call inside a .then() |
| Track calls silently dropped | consentMode: 'required' and grantConsent() not called | Call grantConsent() on user accept |
| Cookie not set on subdomain | cookieDomain mismatch | Pass cookieDomain: '.example.com' explicitly |
| Redundant attribution calls in SPA | Both autoPageTracking: true and manual trackPageView() are active | Pick one; set autoPageTracking: false for manual control (harmless either way — same-URL calls dedupe within a session) |
| SDK no-ops inside an iframe | allowIframe defaults to false | Pass allowIframe: true if you intentionally run inside an iframe |
| Attribution degrades after ~7 days on Safari | Safari ITP caps JS-set first-party cookies | Expected behavior — no workaround; returning visits beyond the cap land as organic |
Enable debug: true in init() to see verbose console logs for all SDK decisions.