@callforge/tracking-client
v0.12.5
Published
Lightweight client library for the CallForge tracking API. Handles location-aware phone number assignment with aggressive caching and preload optimization.
Readme
@callforge/tracking-client
Lightweight client library for the CallForge tracking API. Handles location-aware phone number assignment with aggressive caching and preload optimization.
Installation
npm install @callforge/tracking-client
# or
pnpm add @callforge/tracking-clientLease Hardening Migration (v0.8+)
This release adds bootstrap-token support for lease hardening and bot suppression.
Client integration requirements:
- Add the generated preload snippet in
<head>. This prefetches bootstrap tokens and keeps session lookup fast. - Keep handling
phoneNumberandleaseIdseparately. A request can return a phone number withleaseId: nullwhen lease assignment is intentionally suppressed. - For attribution/scale metrics, treat
leaseIdas the source of truth for lease-backed traffic.
Realtime Data Layer Upgrade (v0.10+)
This release adds explicit browser helpers for the new realtime layer:
client.linkPhoneCall({ phoneNumber, realtime })for strict web-click to phone-call linkage plus optional realtime fields.client.refreshPresenceLink({ phoneNumber, signal })for short-lived active-visitor presence linking.- Automatic presence refreshes on
load, debouncedscroll,focus, visibility return (visibilitychangetovisible), and SPA route changes (enabled by default).
Integration checklist:
- Call
linkPhoneCallimmediately before opening atel:link. - Pass the exact dialed number string (
+1...) used by the link. - Continue dialing even if
linkPhoneCallfails (best-effort attribution assist, not UX-blocking). - When ZIP is known, include it in
linkPhoneCall(...realtime)aswebZipandwebZipSource. - Use
realtime.paramsfor additional key/value fields you want persisted with the linked session. - Keep default auto presence enabled unless you have a specific reason to disable it.
Quick Start
1. Add preload snippet to <head> (required for deterministic leases)
For optimal performance and lease hardening, add this snippet to your HTML <head>:
import { getPreloadSnippet } from '@callforge/tracking-client';
const snippet = getPreloadSnippet({ categoryId: 'your-category-id' });
// Add snippet to your HTML <head>Generated HTML:
<link rel="preconnect" href="https://tracking.callforge.io">
<script>/* preload script */</script>2. Initialize and start session + location requests
import { CallForge } from '@callforge/tracking-client';
const client = CallForge.init({
categoryId: 'your-category-id',
// endpoint: 'https://tracking-dev.callforge.io', // Optional: override for dev
});
const { session, location } = client.getSessionAsync();
// Location is delivered independently (often faster than phone number assignment)
console.log(await location);
// {
// city: "Woodstock",
// state: "Georgia",
// stateCode: "GA",
// zipOptions: ["30188", "30189", "30066", ...] // may be []
// } or null
// Phone session data (deterministic token + phone number)
console.log(await session); // { sessionToken, leaseId, phoneNumber }3. Deterministic click/callback attribution (optional)
If you initiate calls programmatically (click-to-call / callback), you can request a short-lived callIntentToken. CallForge will consume this once to deterministically map the call back to the web session that requested it.
// In the browser:
const { callIntentToken } = await client.createCallIntent();
// Send to your backend and attach to the call you initiate
await fetch('/api/callback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ callIntentToken }),
});Notes:
callIntentTokenis short-lived and single-use.- Treat it as an opaque secret (do not log it).
4. Realtime call-link with bundled realtime fields (recommended)
Use linkPhoneCall to create the strict call-link and optionally attach realtime fields in the same request.
const client = CallForge.init({ categoryId: 'your-category-id' });
async function dialTrackedNumber(phoneNumber: string, webZip?: string) {
try {
// Default TTL is 30s (clamped server-side to 5-120s).
await client.linkPhoneCall({
phoneNumber,
realtime: webZip
? {
webZip,
webZipSource: 'manual', // or 'suggested'
params: {
zipChoiceMethod: 'picker',
},
}
: undefined,
});
} finally {
// Do not block dialing on telemetry failure.
window.location.href = `tel:${phoneNumber}`;
}
}Notes:
linkPhoneCallis optimized for mobile tap-to-call timing.realtime.webZipacceptsmanual(typed) andsuggested(picked from options) viawebZipSource.realtime.webZipis validated client-side (12345format).realtime.paramsallows additional key/value payloads for future realtime enrichment without API changes.
5. GA4 Integration
To enable GA4 call event tracking, CallForge needs the GA4 client_id for the visitor (from the _ga cookie).
Optionally provide your GA4 Measurement ID to improve client ID capture reliability when Google Analytics loads late.
const client = CallForge.init({
categoryId: 'your-category-id',
ga4MeasurementId: 'G-XXXXXXXXXX', // Recommended
});Requirements:
- Google Analytics 4 must be installed on your site and allowed to set the
_gacookie (gtag.js or GTM). - Configure GA4 credentials in CallForge dashboard (Measurement ID + API secret).
How it works:
- Extracts the GA4
client_idfrom the_gacookie and sends it to CallForge asga4ClientId. - Polls briefly after init to capture the cookie if Google Analytics sets it slightly later.
- If
ga4MeasurementIdis configured andgtagis available, also usesgtag('get', measurementId, 'client_id', ...)(with a short retry window). - If
ga4ClientIdbecomes available after a session is created, the client will refresh once to sync it to CallForge.
Manual override:
client.setParams({
ga4ClientId: '1234567890.1234567890',
});6. Track conversion parameters (optional)
The client automatically captures ad platform click IDs from the URL:
gclid- Google Adsgbraid- Google app-to-web (iOS 14+)wbraid- Google web-to-appmsclkid- Microsoft/Bing Adsfbclid- Facebook/Meta Ads
You can also add custom parameters:
client.setParams({
customerId: '456',
landingPage: 'pricing',
});
// Parameters are automatically sent with every API request
await client.getSession();API Reference
CallForge.init(config)
Initialize the tracking client.
interface CallForgeConfig {
categoryId: string; // Required - which number pool to use
endpoint?: string; // Optional - defaults to 'https://tracking.callforge.io'
siteKey?: string; // Optional - cache partition key (defaults to window.location.hostname)
ga4MeasurementId?: string; // Optional - GA4 Measurement ID (e.g., 'G-XXXXXXXXXX')
presenceAutoLink?: { // Optional - defaults shown below
enabled?: boolean; // default true
ttlSeconds?: number; // optional override for auto refreshes
scrollDebounceMs?: number; // default 1200
minRefreshIntervalMs?: number; // default 15000
fireOnLoad?: boolean; // default true
fireOnScroll?: boolean; // default true
fireOnRouteChange?: boolean; // default true (pushState/replaceState/popstate/hashchange)
};
}When presenceAutoLink.enabled is true, the client also sends auto presence refreshes on window focus and when document.visibilityState returns to visible.
client.getSession()
Get tracking session data (phone number + deterministic session token). Returns cached data if valid, otherwise fetches from the API.
interface TrackingSession {
sessionToken: string; // Signed, opaque token used to refresh the session
leaseId: string | null; // Deterministic assignment lease ID (when available)
phoneNumber: string | null;
}Behavior:
- Returns cached data if valid.
- Fetches fresh data when cache is missing/expired.
- If
loc_physical_msis present in the URL, cached sessions are only reused when it matches the cachedlocId. - If lease assignment is suppressed (for example bot traffic or missing/invalid bootstrap),
phoneNumbermay be present whileleaseIdisnull. - Throws on network errors or API errors.
client.getLocation()
Get location data only. Returns cached data if valid, otherwise fetches from the API.
const location = await client.getLocation();
// { city, state, stateCode, zipOptions } or nullinterface TrackingLocation {
city: string;
state: string;
stateCode: string;
zipOptions?: string[]; // ordered by proximity, may be []
}client.getSessionAsync()
Kick off both requests and use each as soon as it resolves.
const { session, location } = client.getSessionAsync();
location.then((loc) => {
// show city/state ASAP
// optionally render loc?.zipOptions in a ZIP picker
});
session.then((sess) => {
// show phone number when ready
});client.createCallIntent()
Create a short-lived call intent token for click/callback deterministic attribution.
const intent = await client.createCallIntent();
console.log(intent.callIntentToken);client.linkPhoneCall(input)
Create a short-lived realtime call-link intent keyed by dialed number, with optional realtime payload fields persisted against the linked session.
const result = await client.linkPhoneCall({
phoneNumber: '+13105551234',
ttlSeconds: 30, // Optional, default 30s
realtime: {
webZip: '30309', // Optional
webZipSource: 'manual', // Optional, defaults to 'manual' when webZip is provided
params: { // Optional
zipChoiceMethod: 'picker',
},
},
});
console.log(result.status); // 'ready'Use this right before tel: navigation so inbound call handling can perform strict 1:1 consume.
Behavior:
- Rejects invalid
realtime.webZipvalues unless they are 5 digits. - Persists bundled realtime values (
webZip+params) during call-link intent creation. - Mirrors bundled realtime values into custom params and performs a best-effort session sync backup.
client.refreshPresenceLink(input)
Refresh a short-lived presence link for active web visitors on a dialed number.
await client.refreshPresenceLink({
phoneNumber: '+13105551234',
signal: 'scroll', // Optional: 'load' | 'scroll' | 'focus' | 'visibility'
ttlSeconds: 45, // Optional, server-clamped
});Note:
- You usually do not need to call this directly because the client auto-refreshes presence on
load, debouncedscroll, and SPA route changes. - Use this method for explicit/manual refresh points only (for example custom engagement events).
client.onReady(callback)
Subscribe to session ready event. Callback is called once session data is available.
client.onReady((session) => {
// session is the same TrackingSession object
});client.onLocationReady(callback)
Subscribe to location ready event. Callback is called once location data is available.
client.onLocationReady((location) => {
// location is { city, state, stateCode, zipOptions } or null
});client.setParams(params)
Set custom tracking parameters for conversion attribution.
client.setParams({
customerId: '456',
landingPage: 'pricing',
campaign: 'summer-sale',
});Behavior:
- Merges with existing params (later calls override earlier values).
- If a session already exists (cached
sessionToken), the client will refresh once to sync updated params server-side (best-effort). - Parameters are sent with every
getSession()API request. - Persisted in localStorage alongside session data.
getPreloadSnippet(config)
Generate HTML snippet for preloading tracking data.
import { getPreloadSnippet } from '@callforge/tracking-client';
const html = getPreloadSnippet({
categoryId: 'your-category-id',
endpoint: 'https://tracking.callforge.io', // Optional
siteKey: 'example.com', // Optional
});Tracking Parameters
Auto-Capture
The client automatically extracts these parameters from the URL:
| Parameter | Source |
|-----------|--------|
| gclid | Google Ads Click ID |
| gbraid | Google app-to-web (iOS 14+) |
| wbraid | Google web-to-app |
| msclkid | Microsoft/Bing Ads Click ID |
| fbclid | Facebook/Meta Ads Click ID |
API Request Format
Parameters are sent as a sorted query string for cache consistency:
/v1/tracking/session?categoryId=cat-123&fbclid=456&gclid=abc&loc_physical_ms=1014221&sessionToken=...Caching Behavior
- Session cache key:
cf_tracking_v1_<siteKey>_<categoryId> - Location cache key:
cf_location_v1_<siteKey> - Bootstrap cache key:
cf_bootstrap_v1_<siteKey>_<categoryId> - TTL: controlled by the server
expiresAtresponse (currently 30 minutes) - Storage: localStorage (falls back to memory if unavailable)
Error Handling
try {
const session = await client.getSession();
if (session.phoneNumber) {
// Use phone number
} else {
// No phone number available
}
} catch (err) {
// Network error or API error
console.error('Failed to get tracking session:', err);
}TypeScript
Full type definitions are included:
import type {
CallForgeConfig,
TrackingSession,
TrackingLocation,
TrackingParams,
ReadyCallback,
LocationReadyCallback,
CallIntentResponse,
CallLinkIntentResponse,
LinkPhoneCallInput,
RealtimeLinkPayload,
RealtimeProfileSource,
} from '@callforge/tracking-client';Environment URLs
| Environment | Endpoint |
|-------------|----------|
| Production | https://tracking.callforge.io (default) |
| Staging | https://tracking-staging.callforge.io |
| Dev | https://tracking-dev.callforge.io |
