Developer docs

API reference / Links

Create a link

POSThttps://app.clik.so/api/links

In short

Makes a new short link that redirects to your destination URL. Only destination_url is required; leave out short_code and Clikso picks a random one.

You need

API key
write permission (Write or Admin). See Authentication.
Role
Editor or admin in the key's workspace.
Plan
Any plan, including Free. Some fields need a paid plan — see plan-gated fields.

Example

curl -X POST "https://app.clik.so/api/links" \
  -H "Authorization: Bearer $CLIKSO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "destination_url": "https://example.com/summer-sale",
    "short_code": "summer-sale",
    "title": "Summer sale",
    "folder_id": "6b7c8d9e-0f1a-4b2c-8d3e-4f5a6b7c8d9e",
    "tags": [
      "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
    ]
  }'
const res = await fetch("https://app.clik.so/api/links", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CLIKSO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "destination_url": "https://example.com/summer-sale",
    "short_code": "summer-sale",
    "title": "Summer sale",
    "folder_id": "6b7c8d9e-0f1a-4b2c-8d3e-4f5a6b7c8d9e",
    "tags": [
      "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
    ]
  }),
});

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/links",
    headers={"Authorization": f"Bearer {os.environ['CLIKSO_API_KEY']}"},
    json={
        "destination_url": "https://example.com/summer-sale",
        "short_code": "summer-sale",
        "title": "Summer sale",
        "folder_id": "6b7c8d9e-0f1a-4b2c-8d3e-4f5a6b7c8d9e",
        "tags": [
            "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
        ],
    },
)

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

Uses requests. Set CLIKSO_API_KEY first.

<?php

$body = <<<'JSON'
{
  "destination_url": "https://example.com/summer-sale",
  "short_code": "summer-sale",
  "title": "Summer sale",
  "folder_id": "6b7c8d9e-0f1a-4b2c-8d3e-4f5a6b7c8d9e",
  "tags": [
    "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
  ]
}
JSON;

$ch = curl_init('https://app.clik.so/api/links');
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(`{
  "destination_url": "https://example.com/summer-sale",
  "short_code": "summer-sale",
  "title": "Summer sale",
  "folder_id": "6b7c8d9e-0f1a-4b2c-8d3e-4f5a6b7c8d9e",
  "tags": [
    "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
  ]
}`)

	req, err := http.NewRequest("POST", "https://app.clik.so/api/links", 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/links')
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("CLIKSO_API_KEY")}"
request["Content-Type"] = "application/json"
request.body = {
  'destination_url' => 'https://example.com/summer-sale',
  'short_code' => 'summer-sale',
  'title' => 'Summer sale',
  'folder_id' => '6b7c8d9e-0f1a-4b2c-8d3e-4f5a6b7c8d9e',
  'tags' => [
    '1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d'
  ]
}.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 = """
            {
              "destination_url": "https://example.com/summer-sale",
              "short_code": "summer-sale",
              "title": "Summer sale",
              "folder_id": "6b7c8d9e-0f1a-4b2c-8d3e-4f5a6b7c8d9e",
              "tags": [
                "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
              ]
            }
            """;

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://app.clik.so/api/links"))
            .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/links");
request.Headers.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("CLIKSO_API_KEY"));
request.Content = new StringContent(
    """
    {
      "destination_url": "https://example.com/summer-sale",
      "short_code": "summer-sale",
      "title": "Summer sale",
      "folder_id": "6b7c8d9e-0f1a-4b2c-8d3e-4f5a6b7c8d9e",
      "tags": [
        "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
      ]
    }
    """,
    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({
  "destination_url": "https://example.com/summer-sale",
  "short_code": "summer-sale",
  "title": "Summer sale",
  "folder_id": "6b7c8d9e-0f1a-4b2c-8d3e-4f5a6b7c8d9e",
  "tags": [
    "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
  ]
})json";
    std::string response;

    curl_easy_setopt(curl, CURLOPT_URL, "https://app.clik.so/api/links");
    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 201
{
  "status": 201,
  "success": true,
  "message": "Link created successfully",
  "data": {
    "id": "3f6c1a2e-9b4d-4e8f-a1c2-7d9e0f1a2b3c",
    "workspace_id": "8a1f2c3d-4b5e-4f60-9a7b-1c2d3e4f5a6b",
    "custom_domain_id": null,
    "domain": "clik.is",
    "domain_status": "default",
    "title": "Summer sale",
    "description": "",
    "short_code": "summer-sale",
    "destination_url": "https://example.com/summer-sale",
    "status": "active",
    "expires_at": null,
    "expired_redirect_url": null,
    "folder_id": "6b7c8d9e-0f1a-4b2c-8d3e-4f5a6b7c8d9e",
    "ab_testing": [],
    "geo_targeting": null,
    "device_targeting": null,
    "qr_metadata": null,
    "og_preview": null,
    "meta_pixel_id": null,
    "tiktok_pixel_id": null,
    "ga4_measurement_id": null,
    "gtm_container_id": null,
    "has_password": false,
    "click_count": 0,
    "created_by": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
    "created_at": "2026-10-09T09:30:00.000Z",
    "updated_at": "2026-10-09T09:30:00.000Z",
    "tags": [
      {
        "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
        "name": "launch",
        "color": "#3b82f6"
      }
    ],
    "creator": {
      "id": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
      "display_name": "Asha Rao",
      "email": "asha@example.com",
      "photo_url": ""
    }
  }
}

Other responses: 401 · 402 · 403 · 404 · 409 · 422 · 429

Request body

  • destination_urlstring · urirequired

    Where the link sends visitors. Must start with http:// or https://.

  • short_codestring

    Your own code: letters, digits and hyphens, 3–30 characters. On the Starter plan and up it may also use / for a nested code like acme/launch: at most 5 parts, no / at the start or end, no //, a letter or digit in every part, slashes counted in the 30. A / on Free gets 402 UPGRADE_REQUIRED. Leave it out for a random 7-character code.

    3–30 characters · Pattern ^(?:[a-zA-Z0-9-]+|-*[a-zA-Z0-9][a-zA-Z0-9-]*(?:/-*[a-zA-Z0-9][a-zA-Z0-9-]*){1,4})$

  • domainstring

    clik.is or another free Clikso domain. For your own domain use custom_domain_id.

    Default "clik.is"

  • custom_domain_idstring · uuid

    The id of one of your verified custom domains. Wins over domain.

  • titlestring

    A name for the link.

    Up to 60 characters

  • descriptionstring

    A note about the link.

    Up to 250 characters

  • statusstring

    The starting status.

    One of: active, archived, expired · Default "active"

  • folder_idstring · uuid

    Put the link in this folder.

  • tagsarray of string · uuid

    Ids of tags to attach.

  • passwordstring or null

    Visitors must type this before they are redirected. Starter plan and up.

    Up to 100 characters

  • expires_atstring · date-time or null

    When the link stops working (ISO 8601).

  • expired_redirect_urlstring · uri or null

    Where visitors go after the link expires.

  • ab_testingarray of ABVariant

    Split visitors between several destinations. Pro plan and up.

  • ab_testing[].urlstring · urirequired

    Where this share of visitors goes.

  • ab_testing[].percentageintegerrequired

    This variant's weight. Weights are relative, so they do not have to add up to 100.

    0–100

  • geo_targetingGeoTargeting

    Destination per country. Keys are 2-letter country codes in capitals (US, IN); values are URLs.

  • device_targetingDeviceTargeting

    Destination per device. Only ios, android and desktop take effect; macos and linux are stored but not used.

  • device_targeting.iosstring · uri

    For iPhone and iPad visitors.

  • device_targeting.androidstring · uri

    For Android visitors.

  • device_targeting.desktopstring · uri

    For visitors not on a phone or tablet.

  • device_targeting.macosstring · uri

    Stored, but not used when redirecting.

  • device_targeting.linuxstring · uri

    Stored, but not used when redirecting.

  • og_previewOgPreview

    The title, description and image social apps show for the short link. Starter plan and up.

  • og_preview.og_titlestring

    The preview title.

  • og_preview.og_descriptionstring

    The preview description.

  • og_preview.og_image_urlstring · uri

    URL of the preview image.

  • qr_metadataQrMetadata

    Saved QR code style. Clikso does not generate QR images through the API.

  • qr_metadata.colorstring

    QR colour.

  • qr_metadata.shapestring

    QR dot shape.

  • qr_metadata.logo_urlstring

    URL of a logo for the QR centre.

  • qr_metadata.paramsobject

    Other QR settings.

  • meta_pixel_idstring or null

    Meta Pixel id: 6–20 digits. Starter plan and up.

    Pattern ^\d{6,20}$

  • tiktok_pixel_idstring or null

    TikTok Pixel id: 8–40 letters and digits. Starter plan and up.

    Pattern ^[A-Za-z0-9]{8,40}$

  • ga4_measurement_idstring or null

    GA4 measurement id, like G-ABC123XYZ. Starter plan and up.

    Pattern ^G-[A-Z0-9]{4,16}$

  • gtm_container_idstring or null

    Google Tag Manager container id, like GTM-ABC1234. Starter plan and up.

    Pattern ^GTM-[A-Z0-9]{4,12}$

Plan-gated fields

Set one of these on a plan below the one listed and you get 402 UPGRADE_REQUIRED, with details.feature, details.field and details.required_plan. On an update, sending the value a link already has, or clearing it, is always allowed.

  • Password protection · passwordStarter and up
  • Custom social preview · og_previewA custom title, description or image.Starter and up
  • Tracking pixels · meta_pixel_id, tiktok_pixel_id, ga4_measurement_id, gtm_container_idStarter and up
  • Geo-targeting · geo_targetingPro and up
  • Device targeting · device_targetingPro and up
  • A/B testing · ab_testingPro and up
  • nestedSlugs · short_codeOnly a code with a `/` in it (a nested code like `acme/launch`). A code without one works on every plan.Starter and up

What you get back

201 with the usual envelope — status, success, message, data. data has these fields:

  • idstring · uuidrequired

    The link's id.

  • workspace_idstring · uuidrequired

    The workspace the link belongs to.

  • custom_domain_idstring · uuid or null

    The custom domain's id, or null on a Clikso domain.

  • domainstringrequired

    The link's domain, like clik.is or links.example.com.

  • domain_statusstringrequired

    default on a Clikso domain, active on a live custom domain, deleted when the custom domain was removed and the link no longer works.

    One of: default, active, deleted

  • titlestringrequired

    A name for the link. Empty string when not set.

    Up to 60 characters

  • descriptionstringrequired

    A note about the link. Empty string when not set.

    Up to 250 characters

  • short_codestringrequired

    The part after the domain in the short URL: 3–30 letters, digits and hyphens, and on Starter and up / for a nested code like acme/launch (at most 5 parts). Up to 20 characters before 1.64.0.

    3–30 characters · Pattern ^(?:[a-zA-Z0-9-]+|-*[a-zA-Z0-9][a-zA-Z0-9-]*(?:/-*[a-zA-Z0-9][a-zA-Z0-9-]*){1,4})$

  • destination_urlstring · urirequired

    Where the short link sends visitors.

  • statusstringrequired

    active links redirect. archived and expired links do not.

    One of: active, archived, expired

  • expires_atstring · date-time or null

    When the link stops working, or null for never.

  • expired_redirect_urlstring · uri or null

    Where visitors go after the link expires, or null for Clikso's expired page.

  • folder_idstring · uuid or null

    The folder the link is in, or null.

  • ab_testingarray of ABVariant

    A/B split destinations. An empty array when not set, never null.

  • ab_testing[].urlstring · urirequired

    Where this share of visitors goes.

  • ab_testing[].percentageintegerrequired

    This variant's weight. Weights are relative, so they do not have to add up to 100.

    0–100

  • geo_targetingGeoTargeting or any or null

    Destination per country, or null.

  • device_targetingDeviceTargeting or any or null

    Destination per device, or null.

  • qr_metadataQrMetadata or any or null

    Saved QR style, or null.

  • og_previewOgPreview or any or null

    Custom social preview, or null.

  • meta_pixel_idstring or null

    Meta Pixel id, or null.

  • tiktok_pixel_idstring or null

    TikTok Pixel id, or null.

  • ga4_measurement_idstring or null

    Google Analytics 4 measurement id, or null.

  • gtm_container_idstring or null

    Google Tag Manager container id, or null.

  • has_passwordbooleanrequired

    true when visitors must enter a password. The password itself is never returned.

  • click_countintegerrequired

    Lifetime clicks on this link.

    Min 0

  • created_bystring · uuid

    The user id of the member who created it.

  • created_atstring · date-timerequired

    When the link was created.

  • updated_atstring · date-time or null

    When the link last changed.

  • tagsarray of TagRefrequired

    The link's tags. An empty array when it has none.

  • tags[].idstring · uuidrequired

    The tag's id.

  • tags[].namestringrequired

    The tag's name.

    Up to 10 characters

  • tags[].colorstring or nullrequired

    The tag's colour as #rrggbb, or an empty string.

  • creatorCreator or any or null

    The member who created the link, or null if unknown.

Common errors

401UNAUTHORIZEDThe API key is missing, malformed or unknown.
402UPGRADE_REQUIREDYou set a field your plan does not include, such as geo_targeting on Free, or a short_code with a / on Free. details names the field and the plan you need.
403FORBIDDENThe key only has Read permission, or its creator is a viewer in this workspace.
404NOT_FOUNDcustom_domain_id is not one of this workspace's domains.
409CONFLICTAnother link already uses this short_code on this domain, or it is a reserved name on a Clikso domain.
422VALIDATION_ERRORA field is missing or malformed (for example a title over 60 characters, a description over 250, or an expires_at that is not a date — details.field names it; a bad short_code is named in details.short_code), the custom domain is not verified, or a folder or tag id is not in this workspace.
422DESTINATION_BLOCKEDA URL in the body points at a link shortener or a blocked site. error.message says which one.
429USAGE_LIMIT_REACHEDYou have created your plan's monthly number of links.
429RATE_LIMITEDThe key went over its per-minute request limit.

Every code and what to do about it: Errors.

Good to know

  • The API does not return a ready-made short URL. Build it yourself: https://{domain}/{short_code}.
  • short_code may use letters, digits and hyphens, 3 to 30 characters (it was 20 before 1.64.0). Underscores and dots are refused. Leave it out and you get a random 7-character code.
  • On the Starter plan and up, short_code may also use / to make a nested code like acme/launch (the short URL is https://clik.is/acme/launch). At most 5 parts (4 slashes), no / at the start or end, no //, and each part needs a letter or digit. The slashes count toward the 30 characters. On Free a / gets 402 UPGRADE_REQUIRED.
  • A bad short_code gets 422 VALIDATION_ERROR saying what is wrong in plain English, for example short_code: Use one slash (/) at a time, not two. — the same sentence is in details.short_code.
  • On clik.is (and the other Clikso domains) some official-sounding names are reserved, such as pricing or login, and so is any nested code that starts with one (pricing/2026). They are refused like a taken code (409). Your own custom domain has no reserved names.
  • domain only accepts clik.is or one of Clikso's free domains (see List free Clikso domains). For your own domain, send its id as custom_domain_id; it must be verified.
  • Keep title to 60 characters and description to 250. password can be up to 100 characters and is never returned — you only see has_password.
  • expires_at must be a date (ISO 8601, for example 2026-12-31T23:59:00Z) and expired_redirect_url at most 2048 characters. A bad value gets 422 VALIDATION_ERROR with details.field naming the field.
  • Every URL in the body is checked: destination_url, expired_redirect_url, and each A/B, country and device URL. A link shortener or a blocked site is refused with DESTINATION_BLOCKED.
  • Each new link counts toward your plan's monthly link limit (Free 50, Starter 500, Pro 5,000, Business 10,000). Over it, you get 429 USAGE_LIMIT_REACHED.
  • There is no bulk create. Send one request per link.
  • Rule order at the edge: country rules first, then device rules (which win over country), and A/B variants only when neither matched. Only the ios, android and desktop device rules take effect.