Skip to main content

Link Analytics

Retrieve detailed click analytics and performance metrics for your links.

Overview

LinkForty provides comprehensive analytics for every link, including:

  • Click counts (total and unique)
  • Geographic distribution
  • Device and platform breakdown
  • Time-based patterns
  • UTM campaign performance
  • Referrer analysis
GET /api/analytics/links/:linkId

Retrieve detailed analytics for a specific link.

Authentication

  • Core (Self-Hosted): Pass userId as a query parameter. See Authentication.
  • Cloud: Requires a valid API key in the Authorization header. See Authentication.

Path Parameters

ParameterTypeRequiredDescription
linkIdstring (UUID)YesLink ID

Query Parameters

ParameterTypeRequiredDefaultDescription
userIdstring (UUID)Core onlyRequired for Core to verify ownership
daysnumberNo30Number of days of data to include (1-365)

Response

{
"totalClicks": 1248,
"uniqueClicks": 892,
"clicksByDate": [
{
"date": "2024-03-15",
"clicks": 45
},
{
"date": "2024-03-14",
"clicks": 38
}
],
"clicksByCountry": [
{
"countryCode": "US",
"country": "United States",
"clicks": 523
},
{
"countryCode": "CA",
"country": "Canada",
"clicks": 187
}
],
"clicksByCity": [
{
"city": "New York",
"countryCode": "US",
"region": "NY",
"clicks": 142
},
{
"city": "Los Angeles",
"countryCode": "US",
"region": "CA",
"clicks": 98
}
],
"clicksByRegion": [
{
"region": "California",
"countryCode": "US",
"clicks": 256
}
],
"clicksByTimezone": [
{
"timezone": "America/New_York",
"clicks": 312
}
],
"clicksByDevice": [
{
"device": "mobile",
"clicks": 742
},
{
"device": "desktop",
"clicks": 398
},
{
"device": "tablet",
"clicks": 108
}
],
"clicksByPlatform": [
{
"platform": "iOS",
"clicks": 623
},
{
"platform": "Android",
"clicks": 425
},
{
"platform": "Windows",
"clicks": 200
}
]
}

Example Requests

Core - Last 30 days (default):

curl "https://your-domain.com/api/analytics/links/a1b2c3d4-e5f6-7890-abcd-ef1234567890?userId=550e8400-e29b-41d4-a716-446655440000"

Cloud - Last 30 days (default):

curl https://api.linkforty.com/api/analytics/links/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-H "Authorization: Bearer $LINKFORTY_API_KEY"

Last 7 days:

curl "https://api.linkforty.com/api/analytics/links/a1b2c3d4-e5f6-7890-abcd-ef1234567890?days=7" \
-H "Authorization: Bearer $LINKFORTY_API_KEY"

Last 90 days:

curl "https://api.linkforty.com/api/analytics/links/a1b2c3d4-e5f6-7890-abcd-ef1234567890?days=90" \
-H "Authorization: Bearer $LINKFORTY_API_KEY"

Get Click Events

GET /api/analytics/clicks/:linkId

Retrieve raw click event data for detailed analysis.

Query Parameters

ParameterTypeRequiredDefaultDescription
limitnumberNo100Maximum events to return (1-1000)

Response

[
{
"id": "click_1",
"linkId": "link_abc123",
"clickedAt": "2024-03-15T14:23:12Z",
"ipAddress": "192.168.1.1",
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)",
"deviceType": "mobile",
"platform": "iOS",
"browser": "Safari",
"browserVersion": "17.0",
"os": "iOS",
"osVersion": "17.0",
"countryCode": "US",
"countryName": "United States",
"region": "California",
"city": "San Francisco",
"timezone": "America/Los_Angeles",
"latitude": 37.7749,
"longitude": -122.4194,
"utmSource": "instagram",
"utmMedium": "social",
"utmCampaign": "spring-sale",
"utmTerm": null,
"utmContent": "story-1",
"referrer": "https://www.instagram.com/"
}
]

Example Request

curl "https://api.linkforty.com/api/analytics/clicks/a1b2c3d4-e5f6-7890-abcd-ef1234567890?limit=50" \
-H "Authorization: Bearer $LINKFORTY_API_KEY"

TypeScript Examples

1. Display Analytics Dashboard

interface LinkAnalytics {
totalClicks: number;
uniqueClicks: number;
clicksByDate: Array<{ date: string; clicks: number }>;
clicksByCountry: Array<{ countryCode: string; country: string; clicks: number }>;
clicksByDevice: Array<{ device: string; clicks: number }>;
clicksByPlatform: Array<{ platform: string; clicks: number }>;
}

async function getLinkAnalytics(linkId: string, days: number = 30): Promise<LinkAnalytics> {
const response = await fetch(
`https://api.linkforty.com/api/analytics/links/${linkId}?days=${days}`,
{
headers: { 'Authorization': `Bearer ${API_KEY}` },
}
);

return response.json();
}

// Usage
const analytics = await getLinkAnalytics('link_id', 7);

console.log(`Total Clicks: ${analytics.totalClicks}`);
console.log(`Unique Visitors: ${analytics.uniqueClicks}`);
console.log(`Top Country: ${analytics.clicksByCountry[0].country}`);
console.log(`Primary Device: ${analytics.clicksByDevice[0].device}`);

2. Calculate Conversion Rate

async function getConversionRate(linkId: string) {
const analytics = await getLinkAnalytics(linkId);
const conversions = 42; // From your conversion tracking

const conversionRate = (conversions / analytics.totalClicks) * 100;

return {
clicks: analytics.totalClicks,
conversions,
rate: conversionRate.toFixed(2) + '%',
};
}
async function compareLinks(linkIds: string[], days: number = 30) {
const results = await Promise.all(
linkIds.map(async id => {
const analytics = await getLinkAnalytics(id, days);
const link = await getLink(id);

return {
linkId: id,
shortCode: link.shortCode,
title: link.title,
clicks: analytics.totalClicks,
uniqueClicks: analytics.uniqueClicks,
ctr: ((analytics.uniqueClicks / analytics.totalClicks) * 100).toFixed(1),
};
})
);

// Sort by clicks
return results.sort((a, b) => b.clicks - a.clicks);
}

// Usage
const comparison = await compareLinks(['link1', 'link2', 'link3']);
console.table(comparison);

4. Generate Analytics Report

async function generateReport(linkId: string, days: number = 30) {
const analytics = await getLinkAnalytics(linkId, days);
const link = await getLink(linkId);

return {
summary: {
linkTitle: link.title,
shortCode: link.shortCode,
period: `Last ${days} days`,
totalClicks: analytics.totalClicks,
uniqueClicks: analytics.uniqueClicks,
avgClicksPerDay: (analytics.totalClicks / days).toFixed(1),
},
geographic: {
topCountries: analytics.clicksByCountry.slice(0, 5),
topCities: analytics.clicksByCity?.slice(0, 5) || [],
},
devices: {
byType: analytics.clicksByDevice,
byPlatform: analytics.clicksByPlatform,
mobilePercentage: (
(analytics.clicksByDevice.find(d => d.device === 'mobile')?.clicks || 0) /
analytics.totalClicks *
100
).toFixed(1),
},
timeline: analytics.clicksByDate,
};
}

5. Export to CSV

async function exportAnalyticsToCSV(linkId: string, days: number = 30) {
const clicks = await fetch(
`https://api.linkforty.com/api/analytics/clicks/${linkId}?limit=1000`,
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
).then(r => r.json());

const csv = [
// Header
'Date,Time,Country,City,Device,Platform,Browser,UTM Source,UTM Campaign',
// Rows
...clicks.map(click => [
new Date(click.clickedAt).toLocaleDateString(),
new Date(click.clickedAt).toLocaleTimeString(),
click.countryName || 'Unknown',
click.city || 'Unknown',
click.deviceType || 'Unknown',
click.platform || 'Unknown',
click.browser || 'Unknown',
click.utmSource || 'Direct',
click.utmCampaign || 'None',
].join(','))
].join('\n');

return csv;
}

// Save to file
const csv = await exportAnalyticsToCSV('link_id');
await fs.writeFile('analytics.csv', csv);

Common Analytics Patterns

1. Track Campaign Performance

async function getCampaignPerformance(linkId: string) {
const analytics = await getLinkAnalytics(linkId);
const clicks = await getClickEvents(linkId, 1000);

// Group by UTM campaign
const byCampaign = clicks.reduce((acc, click) => {
const campaign = click.utmCampaign || 'None';
acc[campaign] = (acc[campaign] || 0) + 1;
return acc;
}, {} as Record<string, number>);

return Object.entries(byCampaign)
.map(([campaign, clicks]) => ({ campaign, clicks }))
.sort((a, b) => b.clicks - a.clicks);
}

2. Find Peak Traffic Times

async function getPeakHours(linkId: string) {
const clicks = await getClickEvents(linkId, 1000);

const byHour = clicks.reduce((acc, click) => {
const hour = new Date(click.clickedAt).getHours();
acc[hour] = (acc[hour] || 0) + 1;
return acc;
}, {} as Record<number, number>);

const sorted = Object.entries(byHour)
.map(([hour, clicks]) => ({ hour: parseInt(hour), clicks }))
.sort((a, b) => b.clicks - a.clicks);

return {
peakHour: sorted[0].hour,
peakClicks: sorted[0].clicks,
distribution: sorted,
};
}

3. Calculate Growth Rate

async function getGrowthRate(linkId: string) {
const last7Days = await getLinkAnalytics(linkId, 7);
const prev7Days = await getLinkAnalytics(linkId, 14);

const currentWeek = last7Days.totalClicks;
const previousWeek = prev7Days.totalClicks - currentWeek;

const growth = ((currentWeek - previousWeek) / previousWeek) * 100;

return {
currentWeek,
previousWeek,
growth: growth.toFixed(1) + '%',
isGrowing: growth > 0,
};
}

4. Detect Geographic Patterns

async function getGeographicInsights(linkId: string) {
const analytics = await getLinkAnalytics(linkId);

const totalClicks = analytics.totalClicks;

return {
topCountry: analytics.clicksByCountry[0],
diversity: analytics.clicksByCountry.length,
concentration: (
(analytics.clicksByCountry[0]?.clicks || 0) / totalClicks * 100
).toFixed(1) + '%',
internationalPercentage: (
analytics.clicksByCountry
.filter(c => c.countryCode !== 'US')
.reduce((sum, c) => sum + c.clicks, 0) /
totalClicks *
100
).toFixed(1) + '%',
};
}

Error Responses

{
"statusCode": 404,
"error": "Not Found",
"message": "Link not found"
}

Invalid Days Parameter

{
"statusCode": 400,
"error": "Bad Request",
"message": "Days must be between 1 and 365"
}

Best Practices

1. Cache Analytics Data

const analyticsCache = new Map<string, { data: LinkAnalytics; timestamp: number }>();

async function getCachedAnalytics(linkId: string, ttl: number = 300000) {
const cached = analyticsCache.get(linkId);

if (cached && Date.now() - cached.timestamp < ttl) {
return cached.data;
}

const data = await getLinkAnalytics(linkId);
analyticsCache.set(linkId, { data, timestamp: Date.now() });

return data;
}

2. Handle Large Date Ranges

async function getAnalyticsInChunks(linkId: string, totalDays: number) {
const chunkSize = 30;
const chunks = Math.ceil(totalDays / chunkSize);

const results = [];

for (let i = 0; i < chunks; i++) {
const days = Math.min(chunkSize, totalDays - i * chunkSize);
const analytics = await getLinkAnalytics(linkId, days);
results.push(analytics);

// Wait between requests
await new Promise(resolve => setTimeout(resolve, 100));
}

return mergeAnalytics(results);
}

3. Real-Time Updates

function useRealtimeAnalytics(linkId: string, refreshInterval: number = 60000) {
const [analytics, setAnalytics] = useState<LinkAnalytics | null>(null);

useEffect(() => {
const fetchAnalytics = async () => {
const data = await getLinkAnalytics(linkId, 1); // Last 24 hours
setAnalytics(data);
};

fetchAnalytics();
const interval = setInterval(fetchAnalytics, refreshInterval);

return () => clearInterval(interval);
}, [linkId, refreshInterval]);

return analytics;
}

Guides