Developer docs

Guides

Click analytics

In short

Three calls read your click data. The summary gives totals and breakdowns. The time series gives clicks per hour, day, week or month. The raw list gives single clicks. All three take the same JSON filters and share one quota per workspace.

You need

An API key
Read permission or higher.
A role
Any member of the workspace.
A plan
Any plan, including Free. Your plan sets how far back you can read and how many calls you can make.

Get a summary

The summary counts every click in a date range. It also breaks them down by country, city, device, browser, operating system, referrer, link and UTM value. This request counts one link's clicks for one week.

curl -X POST "https://app.clik.so/api/analytics/summary" \
  -H "Authorization: Bearer $CLIKSO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "2026-10-01T00:00:00Z",
    "to": "2026-10-08T00:00:00Z",
    "slugs": [
      "clik.is/summer-sale"
    ]
  }'
const res = await fetch("https://app.clik.so/api/analytics/summary", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CLIKSO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "from": "2026-10-01T00:00:00Z",
    "to": "2026-10-08T00:00:00Z",
    "slugs": [
      "clik.is/summer-sale"
    ]
  }),
});

const data = await res.json();
console.log(res.status, data);

Uses fetch (Node 18+ or the browser). Set CLIKSO_API_KEY first.

import os
import requests

response = requests.post(
    "https://app.clik.so/api/analytics/summary",
    headers={"Authorization": f"Bearer {os.environ['CLIKSO_API_KEY']}"},
    json={
        "from": "2026-10-01T00:00:00Z",
        "to": "2026-10-08T00:00:00Z",
        "slugs": [
            "clik.is/summer-sale",
        ],
    },
)

print(response.status_code, response.json())

Uses requests. Set CLIKSO_API_KEY first.

<?php

$body = <<<'JSON'
{
  "from": "2026-10-01T00:00:00Z",
  "to": "2026-10-08T00:00:00Z",
  "slugs": [
    "clik.is/summer-sale"
  ]
}
JSON;

$ch = curl_init('https://app.clik.so/api/analytics/summary');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CLIKSO_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => $body,
]);

$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

echo $status, PHP_EOL, $response, PHP_EOL;

Uses the curl extension. Set CLIKSO_API_KEY first.

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
)

func main() {
	body := strings.NewReader(`{
  "from": "2026-10-01T00:00:00Z",
  "to": "2026-10-08T00:00:00Z",
  "slugs": [
    "clik.is/summer-sale"
  ]
}`)

	req, err := http.NewRequest("POST", "https://app.clik.so/api/analytics/summary", body)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("CLIKSO_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	data, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(res.StatusCode, string(data))
}

Uses net/http. Set CLIKSO_API_KEY first.

require "net/http"
require "json"

uri = URI('https://app.clik.so/api/analytics/summary')
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("CLIKSO_API_KEY")}"
request["Content-Type"] = "application/json"
request.body = {
  'from' => '2026-10-01T00:00:00Z',
  'to' => '2026-10-08T00:00:00Z',
  'slugs' => [
    'clik.is/summer-sale'
  ]
}.to_json

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

puts response.code, response.body

Uses Net::HTTP. Set CLIKSO_API_KEY first.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Main {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "from": "2026-10-01T00:00:00Z",
              "to": "2026-10-08T00:00:00Z",
              "slugs": [
                "clik.is/summer-sale"
              ]
            }
            """;

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.clik.so/api/analytics/summary"))
            .header("Authorization", "Bearer " + System.getenv("CLIKSO_API_KEY"))
            .header("Content-Type", "application/json")
            .method("POST", HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println(response.statusCode());
        System.out.println(response.body());
    }
}

Uses java.net.http (Java 15+). Set CLIKSO_API_KEY first.

using System.Net.Http.Headers;

using var client = new HttpClient();
var request = new HttpRequestMessage(new HttpMethod("POST"), "https://app.clik.so/api/analytics/summary");
request.Headers.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("CLIKSO_API_KEY"));
request.Content = new StringContent(
    """
    {
      "from": "2026-10-01T00:00:00Z",
      "to": "2026-10-08T00:00:00Z",
      "slugs": [
        "clik.is/summer-sale"
      ]
    }
    """,
    System.Text.Encoding.UTF8,
    "application/json");

using var response = await client.SendAsync(request);
Console.WriteLine((int)response.StatusCode);
Console.WriteLine(await response.Content.ReadAsStringAsync());

Uses HttpClient (C# 11+). Set CLIKSO_API_KEY first.

#include <cstdlib>
#include <iostream>
#include <string>
#include <curl/curl.h>

static size_t collect(char* data, size_t size, size_t n, void* out) {
    static_cast<std::string*>(out)->append(data, size * n);
    return size * n;
}

int main() {
    CURL* curl = curl_easy_init();
    if (!curl) return 1;

    const char* key = std::getenv("CLIKSO_API_KEY");
    std::string auth = std::string("Authorization: Bearer ") + (key ? key : "");
    struct curl_slist* headers = curl_slist_append(nullptr, auth.c_str());
    headers = curl_slist_append(headers, "Content-Type: application/json");
    const char* body = R"json({
  "from": "2026-10-01T00:00:00Z",
  "to": "2026-10-08T00:00:00Z",
  "slugs": [
    "clik.is/summer-sale"
  ]
})json";
    std::string response;

    curl_easy_setopt(curl, CURLOPT_URL, "https://app.clik.so/api/analytics/summary");
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, collect);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode rc = curl_easy_perform(curl);
    long status = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (rc != CURLE_OK) {
        std::cerr << curl_easy_strerror(rc) << std::endl;
        return 1;
    }
    std::cout << status << "\n" << response << std::endl;
    return 0;
}

Uses libcurl. Set CLIKSO_API_KEY first.

Response 200
{
  "status": 200,
  "success": true,
  "message": "Success",
  "data": {
    "total_clicks": 412,
    "referrers": [
      {
        "label": "https://www.google.com/",
        "count": 120
      },
      {
        "label": "https://t.co/",
        "count": 64
      }
    ],
    "devices": [
      {
        "label": "mobile",
        "count": 268
      },
      {
        "label": "desktop",
        "count": 131
      },
      {
        "label": "tablet",
        "count": 13
      }
    ],
    "os": [
      {
        "label": "iOS",
        "count": 160
      },
      {
        "label": "Android",
        "count": 108
      },
      {
        "label": "Windows",
        "count": 90
      }
    ],
    "browsers": [
      {
        "label": "Chrome",
        "count": 221
      },
      {
        "label": "Safari",
        "count": 150
      }
    ],
    "short_links": [
      {
        "label": "https://clik.is/summer-sale",
        "count": 412
      }
    ],
    "destinations": [
      {
        "label": "https://example.com/summer-sale",
        "count": 412
      }
    ],
    "countries": [
      {
        "label": "US",
        "count": 201
      },
      {
        "label": "IN",
        "count": 97
      }
    ],
    "cities": [
      {
        "label": "New York",
        "count": 48
      },
      {
        "label": "Bengaluru",
        "count": 41
      }
    ],
    "continents": [
      {
        "label": "NA",
        "count": 230
      },
      {
        "label": "AS",
        "count": 120
      }
    ],
    "timeseries": [
      {
        "timestamp": "2026-10-01T00:00:00Z",
        "clicks": 52
      },
      {
        "timestamp": "2026-10-02T00:00:00Z",
        "clicks": 61
      }
    ],
    "map_data": {
      "US": 201,
      "IN": 97
    },
    "utm": {
      "sources": [
        {
          "label": "newsletter",
          "count": 88
        }
      ],
      "mediums": [
        {
          "label": "email",
          "count": 88
        }
      ],
      "campaigns": [
        {
          "label": "summer",
          "count": 88
        }
      ],
      "terms": [],
      "contents": []
    },
    "link_performance": [
      {
        "link": "https://clik.is/summer-sale",
        "breakdown": [
          {
            "label": "https://example.com/summer-sale",
            "count": 412
          }
        ]
      }
    ],
    "retention_clamped": false,
    "retention_days": 90,
    "effective_from": "2026-10-01T00:00:00.000Z",
    "link_filter_truncated": false,
    "link_filter_limit": 250
  }
}

One summary call answers many questions at once. Use it before you reach for the other two calls.

Get clicks over time

The time series gives click counts in buckets, ready for a chart. Set interval to hour, day, week or month. If you leave it out, you get hourly buckets.

curl -X POST "https://app.clik.so/api/analytics/timeseries" \
  -H "Authorization: Bearer $CLIKSO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "2026-10-01T00:00:00Z",
    "to": "2026-10-08T00:00:00Z",
    "interval": "day"
  }'
const res = await fetch("https://app.clik.so/api/analytics/timeseries", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CLIKSO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "from": "2026-10-01T00:00:00Z",
    "to": "2026-10-08T00:00:00Z",
    "interval": "day"
  }),
});

const data = await res.json();
console.log(res.status, data);

Uses fetch (Node 18+ or the browser). Set CLIKSO_API_KEY first.

import os
import requests

response = requests.post(
    "https://app.clik.so/api/analytics/timeseries",
    headers={"Authorization": f"Bearer {os.environ['CLIKSO_API_KEY']}"},
    json={
        "from": "2026-10-01T00:00:00Z",
        "to": "2026-10-08T00:00:00Z",
        "interval": "day",
    },
)

print(response.status_code, response.json())

Uses requests. Set CLIKSO_API_KEY first.

<?php

$body = <<<'JSON'
{
  "from": "2026-10-01T00:00:00Z",
  "to": "2026-10-08T00:00:00Z",
  "interval": "day"
}
JSON;

$ch = curl_init('https://app.clik.so/api/analytics/timeseries');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CLIKSO_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => $body,
]);

$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

echo $status, PHP_EOL, $response, PHP_EOL;

Uses the curl extension. Set CLIKSO_API_KEY first.

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
)

func main() {
	body := strings.NewReader(`{
  "from": "2026-10-01T00:00:00Z",
  "to": "2026-10-08T00:00:00Z",
  "interval": "day"
}`)

	req, err := http.NewRequest("POST", "https://app.clik.so/api/analytics/timeseries", body)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("CLIKSO_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	data, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(res.StatusCode, string(data))
}

Uses net/http. Set CLIKSO_API_KEY first.

require "net/http"
require "json"

uri = URI('https://app.clik.so/api/analytics/timeseries')
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("CLIKSO_API_KEY")}"
request["Content-Type"] = "application/json"
request.body = {
  'from' => '2026-10-01T00:00:00Z',
  'to' => '2026-10-08T00:00:00Z',
  'interval' => 'day'
}.to_json

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

puts response.code, response.body

Uses Net::HTTP. Set CLIKSO_API_KEY first.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Main {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "from": "2026-10-01T00:00:00Z",
              "to": "2026-10-08T00:00:00Z",
              "interval": "day"
            }
            """;

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.clik.so/api/analytics/timeseries"))
            .header("Authorization", "Bearer " + System.getenv("CLIKSO_API_KEY"))
            .header("Content-Type", "application/json")
            .method("POST", HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println(response.statusCode());
        System.out.println(response.body());
    }
}

Uses java.net.http (Java 15+). Set CLIKSO_API_KEY first.

using System.Net.Http.Headers;

using var client = new HttpClient();
var request = new HttpRequestMessage(new HttpMethod("POST"), "https://app.clik.so/api/analytics/timeseries");
request.Headers.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("CLIKSO_API_KEY"));
request.Content = new StringContent(
    """
    {
      "from": "2026-10-01T00:00:00Z",
      "to": "2026-10-08T00:00:00Z",
      "interval": "day"
    }
    """,
    System.Text.Encoding.UTF8,
    "application/json");

using var response = await client.SendAsync(request);
Console.WriteLine((int)response.StatusCode);
Console.WriteLine(await response.Content.ReadAsStringAsync());

Uses HttpClient (C# 11+). Set CLIKSO_API_KEY first.

#include <cstdlib>
#include <iostream>
#include <string>
#include <curl/curl.h>

static size_t collect(char* data, size_t size, size_t n, void* out) {
    static_cast<std::string*>(out)->append(data, size * n);
    return size * n;
}

int main() {
    CURL* curl = curl_easy_init();
    if (!curl) return 1;

    const char* key = std::getenv("CLIKSO_API_KEY");
    std::string auth = std::string("Authorization: Bearer ") + (key ? key : "");
    struct curl_slist* headers = curl_slist_append(nullptr, auth.c_str());
    headers = curl_slist_append(headers, "Content-Type: application/json");
    const char* body = R"json({
  "from": "2026-10-01T00:00:00Z",
  "to": "2026-10-08T00:00:00Z",
  "interval": "day"
})json";
    std::string response;

    curl_easy_setopt(curl, CURLOPT_URL, "https://app.clik.so/api/analytics/timeseries");
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, collect);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode rc = curl_easy_perform(curl);
    long status = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (rc != CURLE_OK) {
        std::cerr << curl_easy_strerror(rc) << std::endl;
        return 1;
    }
    std::cout << status << "\n" << response << std::endl;
    return 0;
}

Uses libcurl. Set CLIKSO_API_KEY first.

Response 200
{
  "status": 200,
  "success": true,
  "message": "Success",
  "data": [
    {
      "timestamp": "2026-10-01T00:00:00Z",
      "clicks": 52
    },
    {
      "timestamp": "2026-10-02T00:00:00Z",
      "clicks": 61
    },
    {
      "timestamp": "2026-10-04T00:00:00Z",
      "clicks": 47
    }
  ]
}

Buckets with no clicks are left out. Fill those gaps with zero when you draw the chart.

List raw clicks

The raw list gives single clicks, newest first. Each click has its time, link, country, device, browser, operating system and referrer.

curl -X POST "https://app.clik.so/api/analytics/filter" \
  -H "Authorization: Bearer $CLIKSO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "2026-10-07T00:00:00Z",
    "to": "2026-10-08T00:00:00Z",
    "country": [
      "US"
    ]
  }'
const res = await fetch("https://app.clik.so/api/analytics/filter", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CLIKSO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "from": "2026-10-07T00:00:00Z",
    "to": "2026-10-08T00:00:00Z",
    "country": [
      "US"
    ]
  }),
});

const data = await res.json();
console.log(res.status, data);

Uses fetch (Node 18+ or the browser). Set CLIKSO_API_KEY first.

import os
import requests

response = requests.post(
    "https://app.clik.so/api/analytics/filter",
    headers={"Authorization": f"Bearer {os.environ['CLIKSO_API_KEY']}"},
    json={
        "from": "2026-10-07T00:00:00Z",
        "to": "2026-10-08T00:00:00Z",
        "country": [
            "US",
        ],
    },
)

print(response.status_code, response.json())

Uses requests. Set CLIKSO_API_KEY first.

<?php

$body = <<<'JSON'
{
  "from": "2026-10-07T00:00:00Z",
  "to": "2026-10-08T00:00:00Z",
  "country": [
    "US"
  ]
}
JSON;

$ch = curl_init('https://app.clik.so/api/analytics/filter');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('CLIKSO_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => $body,
]);

$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

echo $status, PHP_EOL, $response, PHP_EOL;

Uses the curl extension. Set CLIKSO_API_KEY first.

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
)

func main() {
	body := strings.NewReader(`{
  "from": "2026-10-07T00:00:00Z",
  "to": "2026-10-08T00:00:00Z",
  "country": [
    "US"
  ]
}`)

	req, err := http.NewRequest("POST", "https://app.clik.so/api/analytics/filter", body)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("CLIKSO_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	data, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(res.StatusCode, string(data))
}

Uses net/http. Set CLIKSO_API_KEY first.

require "net/http"
require "json"

uri = URI('https://app.clik.so/api/analytics/filter')
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("CLIKSO_API_KEY")}"
request["Content-Type"] = "application/json"
request.body = {
  'from' => '2026-10-07T00:00:00Z',
  'to' => '2026-10-08T00:00:00Z',
  'country' => [
    'US'
  ]
}.to_json

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

puts response.code, response.body

Uses Net::HTTP. Set CLIKSO_API_KEY first.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Main {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "from": "2026-10-07T00:00:00Z",
              "to": "2026-10-08T00:00:00Z",
              "country": [
                "US"
              ]
            }
            """;

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.clik.so/api/analytics/filter"))
            .header("Authorization", "Bearer " + System.getenv("CLIKSO_API_KEY"))
            .header("Content-Type", "application/json")
            .method("POST", HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println(response.statusCode());
        System.out.println(response.body());
    }
}

Uses java.net.http (Java 15+). Set CLIKSO_API_KEY first.

using System.Net.Http.Headers;

using var client = new HttpClient();
var request = new HttpRequestMessage(new HttpMethod("POST"), "https://app.clik.so/api/analytics/filter");
request.Headers.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("CLIKSO_API_KEY"));
request.Content = new StringContent(
    """
    {
      "from": "2026-10-07T00:00:00Z",
      "to": "2026-10-08T00:00:00Z",
      "country": [
        "US"
      ]
    }
    """,
    System.Text.Encoding.UTF8,
    "application/json");

using var response = await client.SendAsync(request);
Console.WriteLine((int)response.StatusCode);
Console.WriteLine(await response.Content.ReadAsStringAsync());

Uses HttpClient (C# 11+). Set CLIKSO_API_KEY first.

#include <cstdlib>
#include <iostream>
#include <string>
#include <curl/curl.h>

static size_t collect(char* data, size_t size, size_t n, void* out) {
    static_cast<std::string*>(out)->append(data, size * n);
    return size * n;
}

int main() {
    CURL* curl = curl_easy_init();
    if (!curl) return 1;

    const char* key = std::getenv("CLIKSO_API_KEY");
    std::string auth = std::string("Authorization: Bearer ") + (key ? key : "");
    struct curl_slist* headers = curl_slist_append(nullptr, auth.c_str());
    headers = curl_slist_append(headers, "Content-Type: application/json");
    const char* body = R"json({
  "from": "2026-10-07T00:00:00Z",
  "to": "2026-10-08T00:00:00Z",
  "country": [
    "US"
  ]
})json";
    std::string response;

    curl_easy_setopt(curl, CURLOPT_URL, "https://app.clik.so/api/analytics/filter");
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, collect);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

    CURLcode rc = curl_easy_perform(curl);
    long status = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    if (rc != CURLE_OK) {
        std::cerr << curl_easy_strerror(rc) << std::endl;
        return 1;
    }
    std::cout << status << "\n" << response << std::endl;
    return 0;
}

Uses libcurl. Set CLIKSO_API_KEY first.

Response 200
{
  "status": 200,
  "success": true,
  "message": "Success",
  "data": [
    {
      "timestamp": "2026-10-07T18:42:10Z",
      "workspace_id": "8a1f2c3d-4b5e-4f60-9a7b-1c2d3e4f5a6b",
      "url_id": "3f6c1a2e-9b4d-4e8f-a1c2-7d9e0f1a2b3c",
      "short_url": "https://clik.is/summer-sale",
      "destination_url": "https://example.com/summer-sale",
      "country": "US",
      "device": "mobile",
      "browser": "Safari",
      "os": "iOS",
      "referrer": "https://t.co/"
    }
  ]
}

You get at most the 1,000 most recent clicks that match. There is no next page, so use a shorter date range to see older clicks.

Choose which clicks to count

All three calls take the same JSON body. Every field is optional.

A request body with filters
{
  "from": "2026-10-01T00:00:00Z",
  "to": "2026-10-08T00:00:00Z",
  "slugs": [
    "clik.is/summer-sale"
  ],
  "country": [
    "US",
    "IN"
  ],
  "device": [
    "mobile"
  ]
}
  • from and to set the time window, as ISO 8601 dates in UTC. If either one is missing, you get the last 7 days. A value that is not a date, or a from after to, gets a 422 before the call counts toward your quota.
  • url_ids, slugs and domains pick links. A slug is written as domain/short_code, like clik.is/summer-sale.
  • country, city, region, continent, device, os, browser and referrer filter by where and how people clicked.
  • short_links and destination_urls filter by the exact URL. Short links include https://.
  • utm_source, utm_medium, utm_campaign, utm_term and utm_content filter by the UTM value recorded for the click.

Different filters combine with AND. Values inside one list combine with OR. So the body above means: this link, and the United States or India, and phones.

Each list can hold up to 100 values. Country codes have two letters, like US. Device values are mobile, tablet, desktop and tv.

How far back you can read

Your plan keeps clicks for a set number of days.

Click history per plan

Click history you can readOlder clicks are left out, not an error
30 days

If from is older than that, Clikso moves it forward to the oldest day your plan keeps. This is not an error. The response tells you it happened:

  • The summary has retention_clamped, retention_days and effective_from in data.
  • All three calls send the headers X-Analytics-Retention-Clamped, X-Analytics-Retention-Days and X-Analytics-Effective-From.

Quota and caching

Analytics calls have their own quota. It is counted per workspace, so all your keys share it.

Analytics calls per workspace

Analytics requests per minutePer workspace, shared by all keys
5
Analytics requests per dayPer workspace, resets at 00:00 UTC
100

Every response carries headers that show where you stand:

  • X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset for the current minute.
  • X-Analytics-Quota-Limit and X-Analytics-Quota-Remaining for the day. The day resets at 00:00 UTC.

The same request within 60 seconds is often answered from a cache. It is faster, but it still counts toward your quota. The X-Analytics-Cache header says HIT or MISS.

What you get back

  • Summary: data holds total_clicks, then one list per breakdown. Each list item is a label and a count. You also get timeseries (clicks per day), map_data (country code to clicks), utm and link_performance.
  • Time series: data is a list of timestamp and clicks pairs, oldest first. Each timestamp is the start of its bucket, in UTC.
  • Raw clicks: data is a list of clicks, newest first. Fields with no value are empty strings.

Common errors

401UNAUTHORIZEDThe key is missing or wrong.Send Authorization: Bearer clik_live_… with a key from Settings → API.
403FORBIDDENThe Workspace-Id header names a different workspace than the key.Check the key’s permission and your role. If you send Workspace-Id, it must match the key’s workspace.
422VALIDATION_ERRORfrom or to is not an ISO 8601 date, from is after to, interval is not hour, day, week or month, a filter list is too long, or the body is not JSON.Fix the field and send the request again.
429RATE_LIMITEDToo many analytics calls this minute.Wait the number of seconds in Retry-After.
429USAGE_LIMIT_REACHEDYour workspace used today's analytics quota.Wait until error.details.resets_at (00:00 UTC), or upgrade.
500INTERNAL_ERRORThe analytics store did not answer.Try again in a moment.

Good to know

  • New clicks show up within about a minute.
  • Each click is counted once, even when its delivery was retried.
  • There is no unique-visitor count and no session count. Every visit is a click.
  • Bots are not filtered out.
  • Country and city come from the visitor's IP address. A VPN or a work network can change them.
  • Each summary breakdown lists the top 250 values. link_performance lists the top 500.
  • A domains filter covers at most 250 links. When it hits that, link_filter_truncated is true and the numbers cover only those links.
  • The summary's own timeseries is always per day. For other buckets, use the time series call.
  • A request with a bad body (a 422) does not use your quota. Neither does a call refused by the per-minute limit.
  • The AI analytics chat is only in the dashboard. It is not part of the API.