Skip to content
Tolinku
Tolinku
Sign In Start Free
Analytics & Attribution · · 6 min read

Analytics API Integration for Deep Links

By Tolinku Staff
|
Tolinku mobile attribution dashboard screenshot for analytics blog posts

Dashboards are useful until you need to combine deep link analytics with data from other systems. An analytics API lets you pull click data, conversion metrics, and campaign performance into your own tools: custom dashboards, BI platforms, Slack bots, or automated reporting pipelines.

This guide covers how to integrate deep link analytics via API. For data export options, see exporting deep link analytics data. For analytics fundamentals, see deep link analytics: measuring what matters.

Tolinku analytics dashboard showing click metrics and conversion funnel The analytics dashboard with date range selector, filters, charts, and breakdowns.

API Design Patterns

REST API for Analytics

A typical analytics REST API exposes endpoints for querying click data, conversions, and aggregated metrics:

GET /v1/analytics/clicks          # Raw click events
GET /v1/analytics/clicks/summary  # Aggregated click metrics
GET /v1/analytics/conversions     # Conversion events
GET /v1/analytics/campaigns       # Campaign-level metrics
GET /v1/analytics/routes          # Route-level metrics

Authentication

Analytics APIs typically use API keys or OAuth tokens:

// API key authentication
const headers = {
  'Authorization': `Bearer ${process.env.ANALYTICS_API_KEY}`,
  'Content-Type': 'application/json'
};

// Make an authenticated request
async function queryAnalytics(endpoint: string, params: Record<string, string>) {
  const url = new URL(`https://api.example.com/v1/analytics/${endpoint}`);
  Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));

  const response = await fetch(url.toString(), { headers });

  if (!response.ok) {
    throw new Error(`Analytics API error: ${response.status} ${response.statusText}`);
  }

  return response.json();
}

Common Query Patterns

Click Summary by Date Range

The most common query: how many clicks, opens, and conversions occurred in a date range?

interface ClickSummary {
  total_clicks: number;
  app_opens: number;
  open_rate: number;
  fallbacks: number;
  fallback_rate: number;
  conversions: number;
  conversion_rate: number;
}

async function getClickSummary(
  start: string,
  end: string,
  filters?: { campaign?: string; route?: string; platform?: string }
): Promise<ClickSummary> {
  const params: Record<string, string> = { start, end };
  if (filters?.campaign) params.campaign = filters.campaign;
  if (filters?.route) params.route = filters.route;
  if (filters?.platform) params.platform = filters.platform;

  return queryAnalytics('clicks/summary', params);
}

// Usage
const summary = await getClickSummary('2026-07-01', '2026-07-19', {
  campaign: 'summer-promo'
});
console.log(`${summary.total_clicks} clicks, ${summary.conversion_rate}% conversion`);

Time Series Data

Get metrics over time for charting:

interface TimeSeriesPoint {
  date: string;
  clicks: number;
  app_opens: number;
  conversions: number;
}

async function getTimeSeries(
  start: string,
  end: string,
  granularity: 'hour' | 'day' | 'week' | 'month'
): Promise<TimeSeriesPoint[]> {
  return queryAnalytics('clicks/timeseries', {
    start,
    end,
    granularity
  });
}

Campaign Comparison

Compare performance across campaigns:

interface CampaignMetrics {
  campaign: string;
  clicks: number;
  app_opens: number;
  open_rate: number;
  conversions: number;
  conversion_rate: number;
  cpa: number;
}

async function compareCampaigns(
  start: string,
  end: string
): Promise<CampaignMetrics[]> {
  return queryAnalytics('campaigns', {
    start,
    end,
    sort: 'conversions',
    order: 'desc'
  });
}

Funnel Analysis

Query step-by-step conversion data:

interface FunnelStep {
  step: string;
  count: number;
  rate: number;
  drop_off: number;
}

async function getFunnel(
  campaign: string,
  start: string,
  end: string
): Promise<FunnelStep[]> {
  return queryAnalytics('funnels', {
    campaign,
    start,
    end,
    steps: 'click,app_open,target_screen,conversion'
  });
}

Building Custom Dashboards

Fetching Data for a Dashboard

interface DashboardData {
  summary: ClickSummary;
  timeSeries: TimeSeriesPoint[];
  topCampaigns: CampaignMetrics[];
  topRoutes: RouteMetrics[];
  platformBreakdown: PlatformMetrics[];
}

async function fetchDashboardData(dateRange: { start: string; end: string }): Promise<DashboardData> {
  // Fetch all dashboard data in parallel
  const [summary, timeSeries, topCampaigns, topRoutes, platformBreakdown] = await Promise.all([
    getClickSummary(dateRange.start, dateRange.end),
    getTimeSeries(dateRange.start, dateRange.end, 'day'),
    compareCampaigns(dateRange.start, dateRange.end),
    queryAnalytics('routes', { start: dateRange.start, end: dateRange.end, limit: '10' }),
    queryAnalytics('clicks/summary', {
      start: dateRange.start,
      end: dateRange.end,
      group_by: 'platform'
    })
  ]);

  return { summary, timeSeries, topCampaigns, topRoutes, platformBreakdown };
}

Caching API Responses

Analytics data for past dates does not change. Cache aggressively:

const cache = new Map<string, { data: any; expires: number }>();

async function cachedQuery(
  endpoint: string,
  params: Record<string, string>,
  ttlMs: number = 300000 // 5 minutes default
): Promise<any> {
  const cacheKey = `${endpoint}:${JSON.stringify(params)}`;
  const cached = cache.get(cacheKey);

  if (cached && cached.expires > Date.now()) {
    return cached.data;
  }

  const data = await queryAnalytics(endpoint, params);

  // Cache historical data longer (1 hour) vs recent data (5 minutes)
  const isHistorical = new Date(params.end) < new Date(Date.now() - 86400000);
  const ttl = isHistorical ? 3600000 : ttlMs;

  cache.set(cacheKey, { data, expires: Date.now() + ttl });
  return data;
}

Automated Reporting

Slack Integration

Post daily analytics summaries to Slack:

async function postDailySlackReport(webhookUrl: string) {
  const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0];
  const summary = await getClickSummary(yesterday, yesterday);

  const previousDay = new Date(Date.now() - 172800000).toISOString().split('T')[0];
  const previous = await getClickSummary(previousDay, previousDay);

  const clickChange = ((summary.total_clicks - previous.total_clicks) / previous.total_clicks * 100).toFixed(1);
  const convChange = ((summary.conversions - previous.conversions) / previous.conversions * 100).toFixed(1);

  await fetch(webhookUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: `*Daily Deep Link Report: ${yesterday}*\n` +
        `Clicks: ${summary.total_clicks.toLocaleString()} (${clickChange}% vs prev day)\n` +
        `App Opens: ${summary.app_opens.toLocaleString()} (${summary.open_rate}% rate)\n` +
        `Conversions: ${summary.conversions.toLocaleString()} (${convChange}% vs prev day)\n` +
        `Conversion Rate: ${summary.conversion_rate}%`
    })
  });
}

Email Report Generation

Generate HTML email reports from API data:

async function generateEmailReport(
  recipients: string[],
  dateRange: { start: string; end: string }
) {
  const data = await fetchDashboardData(dateRange);

  const html = `
    <h2>Deep Link Performance: ${dateRange.start} to ${dateRange.end}</h2>
    <table>
      <tr><td>Total Clicks</td><td>${data.summary.total_clicks.toLocaleString()}</td></tr>
      <tr><td>App Opens</td><td>${data.summary.app_opens.toLocaleString()} (${data.summary.open_rate}%)</td></tr>
      <tr><td>Conversions</td><td>${data.summary.conversions.toLocaleString()} (${data.summary.conversion_rate}%)</td></tr>
    </table>
    <h3>Top Campaigns</h3>
    <table>
      <tr><th>Campaign</th><th>Clicks</th><th>Conversions</th><th>Rate</th></tr>
      ${data.topCampaigns.slice(0, 5).map(c =>
        `<tr><td>${c.campaign}</td><td>${c.clicks}</td><td>${c.conversions}</td><td>${c.conversion_rate}%</td></tr>`
      ).join('')}
    </table>
  `;

  await sendEmail({ to: recipients, subject: `Deep Link Report: ${dateRange.start}`, html });
}

Alerting

Trigger alerts when metrics cross thresholds:

interface AlertRule {
  metric: string;
  condition: 'above' | 'below';
  threshold: number;
  window: string; // e.g., '1h', '24h'
  notify: string[]; // Slack channels, email addresses
}

async function checkAlerts(rules: AlertRule[]) {
  for (const rule of rules) {
    const end = new Date().toISOString();
    const start = subtractDuration(end, rule.window);

    const summary = await getClickSummary(start, end);
    const value = summary[rule.metric as keyof ClickSummary] as number;

    const triggered = rule.condition === 'above'
      ? value > rule.threshold
      : value < rule.threshold;

    if (triggered) {
      await sendAlert(rule, value);
    }
  }
}

// Example rules
const alertRules: AlertRule[] = [
  {
    metric: 'open_rate',
    condition: 'below',
    threshold: 50,
    window: '1h',
    notify: ['#deep-links-alerts']
  },
  {
    metric: 'fallback_rate',
    condition: 'above',
    threshold: 40,
    window: '1h',
    notify: ['#deep-links-alerts', '[email protected]']
  }
];

Error Handling

Retry Logic

Analytics APIs may return temporary errors. Implement retry with exponential backoff:

async function queryWithRetry(
  endpoint: string,
  params: Record<string, string>,
  maxRetries: number = 3
): Promise<any> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(buildUrl(endpoint, params), { headers });

      if (response.status === 429) {
        // Rate limited: wait and retry
        const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
        await sleep(retryAfter * 1000);
        continue;
      }

      if (response.status >= 500) {
        // Server error: exponential backoff
        await sleep(Math.pow(2, attempt) * 1000);
        continue;
      }

      if (!response.ok) {
        throw new Error(`API error: ${response.status}`);
      }

      return response.json();
    } catch (error) {
      if (attempt === maxRetries) throw error;
      await sleep(Math.pow(2, attempt) * 1000);
    }
  }
}

Handling Incomplete Data

Analytics data may have delays. Account for data freshness:

interface QueryMetadata {
  data_freshness: string; // ISO timestamp of latest data
  is_complete: boolean;   // Whether the date range has complete data
  warning?: string;       // e.g., "Data for today may be incomplete"
}

async function queryWithFreshness(
  endpoint: string,
  params: Record<string, string>
): Promise<{ data: any; metadata: QueryMetadata }> {
  const response = await queryAnalytics(endpoint, params);

  return {
    data: response.data,
    metadata: {
      data_freshness: response.metadata.freshness,
      is_complete: response.metadata.complete,
      warning: response.metadata.warning
    }
  };
}

Third-Party Integrations

BI Tool Integration

Connect analytics APIs to BI tools like Looker, Tableau, or Metabase:

BI Tool Connection Method Notes
Looker Custom connector or REST API data source Real-time queries
Tableau Web data connector Scheduled refresh
Metabase Direct database connection or API Point to your data warehouse
Google Data Studio Sheets or BigQuery connector Export to Sheets/BigQuery first

Data Pipeline Tools

Use ETL/ELT tools to automate data movement:

Tool Approach
Airbyte Custom connector for analytics API
Fivetran Custom connector or webhook ingestion
dbt Transform data after loading to warehouse
Custom script Direct API calls on a cron schedule

Tolinku for Analytics API

Tolinku's analytics API provides programmatic access to click data, campaign metrics, and conversion events. Query analytics data for custom dashboards, automated reports, and data warehouse integration. See the API reference for available endpoints.

For data export options, see exporting deep link analytics data. For building short links programmatically, see short link APIs: programmatic link creation.

Get deep linking tips in your inbox

One email per week. No spam.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.