Skip to main content

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
How this differs from mobile attribution

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 install @chottulink/web-sdk
import ChottuLink from '@chottulink/web-sdk';

await ChottuLink.init({ apiKey: 'YOUR_PUBLIC_API_KEY' });

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.

note

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' },
});
Deduping retries

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:

ParameterSource
cl_link_idChottuLink deep link
utm_source, utm_medium, utm_campaign, utm_term, utm_contentUTM tags
fbclidFacebook / Meta Ads
gclid, gbraid, wbraidGoogle Ads
ttclidTikTok Ads

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).

This is not page-view analytics

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();
warning

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

SymptomLikely causeFix
No attribution recordedPage loaded without attribution params in the URLCheck the original landing URL; getAttribution() returns null on organic traffic
getAttribution() always nullCalled before init() resolvesawait init() first, or call inside a .then()
Track calls silently droppedconsentMode: 'required' and grantConsent() not calledCall grantConsent() on user accept
Cookie not set on subdomaincookieDomain mismatchPass cookieDomain: '.example.com' explicitly
Redundant attribution calls in SPABoth autoPageTracking: true and manual trackPageView() are activePick one; set autoPageTracking: false for manual control (harmless either way — same-URL calls dedupe within a session)
SDK no-ops inside an iframeallowIframe defaults to falsePass allowIframe: true if you intentionally run inside an iframe
Attribution degrades after ~7 days on SafariSafari ITP caps JS-set first-party cookiesExpected 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.