Developer docs

API reference / Analytics

Get clicks over time

POSThttps://app.clik.so/api/analytics/timeseries

In short

Returns click counts in time buckets of an hour, a day, a week or a month, for drawing a chart. It takes the same filters as the click summary.

You need

API key
read permission or higher. See Authentication.
Role
Any member (viewer and up) in the key's workspace.
Plan
Any plan, including Free.

Example

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
    }
  ]
}

Other responses: 401 · 422 · 429 · 500

Request body

  • fromstring · date-time

    Start of the window, inclusive. An ISO 8601 date-time with a time zone, like 2026-10-01T00:00:00Z. With to; if either is missing, the last 7 days are used. Anything that is not a valid date-time, or a from after to, is a 422.

  • tostring · date-time

    End of the window, exclusive. An ISO 8601 date-time with a time zone, like 2026-10-08T00:00:00Z.

  • intervalstring

    Bucket size. Only used by the time-series endpoint.

    One of: hour, day, week, month · Default "hour"

  • url_idsarray of string · uuid

    Only these links, by id.

    Up to 100 items

  • slugsarray of string

    Only these links, as domain/short_code, like clik.is/summer-sale or, for a nested code, clik.is/acme/launch (split at the first /).

    Up to 100 items

  • domainsarray of string

    Only links on these domains. Covers at most 250 links.

    Up to 100 items

  • countryarray of string

    2-letter country codes, like US.

    Up to 100 items

  • cityarray of string

    City names, as they appear in the summary.

    Up to 100 items

  • regionarray of string

    Region names.

    Up to 100 items

  • continentarray of string

    Continent codes: AF, AN, AS, EU, NA, OC, SA.

    Up to 100 items

  • devicearray of string

    mobile, tablet, desktop or tv.

    Up to 100 items

  • osarray of string

    Operating systems, like iOS, Android, Windows, Mac OS, Linux.

    Up to 100 items

  • browserarray of string

    Browsers, like Chrome, Safari, Firefox.

    Up to 100 items

  • referrerarray of string

    Exact referrer URLs, as they appear in the summary.

    Up to 100 items

  • short_linksarray of string

    Full short URLs, like https://clik.is/summer-sale.

    Up to 100 items

  • destination_urlsarray of string

    Exact destination URLs.

    Up to 100 items

  • utm_sourcearray of string

    utm_source values on the clicked short URL.

    Up to 100 items

  • utm_mediumarray of string

    utm_medium values on the clicked short URL.

    Up to 100 items

  • utm_campaignarray of string

    utm_campaign values on the clicked short URL.

    Up to 100 items

  • utm_termarray of string

    utm_term values on the clicked short URL.

    Up to 100 items

  • utm_contentarray of string

    utm_content values on the clicked short URL.

    Up to 100 items

What you get back

200 with the usual envelope — status, success, message, data. data is an array; each item has these fields:

  • timestampstring · date-timerequired

    Start of the bucket, in UTC.

  • clicksintegerrequired

    Clicks in the bucket.

    Min 0

Response headers

  • X-RateLimit-Limitinteger

    Analytics calls allowed per minute for this workspace.

  • X-RateLimit-Remaininginteger

    Analytics calls left in the current minute.

  • X-RateLimit-Resetinteger

    Seconds until the current minute window resets.

  • X-Analytics-Quota-Limitinteger

    Analytics calls allowed per UTC day for this workspace.

  • X-Analytics-Quota-Remaininginteger

    Analytics calls left today (UTC).

  • X-Analytics-Retention-Clampedstring

    true when from was older than your plan keeps and was moved forward.

  • X-Analytics-Retention-Daysinteger

    How many days of clicks your plan keeps.

  • X-Analytics-Effective-Fromstring

    The start of the window that was actually used, after any move forward (ISO 8601).

  • X-Analytics-Link-Filter-Truncatedstring

    true when a link filter matched more links than one query can cover, so the numbers are for a subset.

  • X-Analytics-Link-Filter-Limitinteger

    The most links one analytics query can cover (250).

Common errors

401UNAUTHORIZEDThe API key is missing, malformed or unknown.
422VALIDATION_ERRORinterval is not hour, day, week or month, the body is not JSON, a filter list has more than 100 entries, from or to is not an ISO 8601 date-time, or from is after to.
429RATE_LIMITEDThe workspace went over its per-minute analytics quota, or the key over its per-minute limit. Wait for Retry-After seconds.
429USAGE_LIMIT_REACHEDThe workspace used its daily analytics quota. details.resets_at says when it resets (midnight UTC).
500INTERNAL_ERRORThe analytics store did not answer. Try again in a moment.

Every code and what to do about it: Errors.

Good to know

  • Rate limit: Analytics quota per workspace, shared by all its keys: per minute Free 5, Starter 10, Pro 30, Business 60; per UTC day Free 100, Starter 1,000, Pro 5,000, Business 20,000.
  • interval is hour, day, week or month, and defaults to hour. Any other value is a 422.
  • Buckets with no clicks are left out, so fill the gaps with zero when you draw a chart.
  • Timestamps are the start of each bucket, in UTC. Weeks start on Sunday.
  • Leave out from or to and you get the last 7 days. A from older than your plan keeps is moved forward; the X-Analytics-Retention-Clamped header says when.
  • The same request within 60 seconds is answered from a cache. It still counts toward your quota.