API reference / Links
Create a link
https://app.clik.so/api /linksIn short
destination_url is required; leave out short_code and Clikso picks a random one.You need
- API key
writepermission (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.bodyUses 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.
{
"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": ""
}
}
}Request body
destination_urlstring · urirequiredWhere the link sends visitors. Must start with
http://orhttps://.short_codestringYour own code: letters, digits and hyphens, 3–30 characters. On the Starter plan and up it may also use
/for a nested code likeacme/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 gets402 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})$domainstringclik.isor another free Clikso domain. For your own domain usecustom_domain_id.Default
"clik.is"custom_domain_idstring · uuidThe id of one of your verified custom domains. Wins over
domain.titlestringA name for the link.
Up to 60 characters
descriptionstringA note about the link.
Up to 250 characters
statusstringThe starting status.
One of:
active,archived,expired· Default"active"folder_idstring · uuidPut the link in this folder.
tagsarray of string · uuidIds of tags to attach.
passwordstring or nullVisitors must type this before they are redirected. Starter plan and up.
Up to 100 characters
expires_atstring · date-time or nullWhen the link stops working (ISO 8601).
expired_redirect_urlstring · uri or nullWhere visitors go after the link expires.
ab_testingarray of ABVariantSplit visitors between several destinations. Pro plan and up.
ab_testing[].urlstring · urirequiredWhere this share of visitors goes.
ab_testing[].percentageintegerrequiredThis variant's weight. Weights are relative, so they do not have to add up to 100.
0–100
geo_targetingGeoTargetingDestination per country. Keys are 2-letter country codes in capitals (
US,IN); values are URLs.device_targetingDeviceTargetingDestination per device. Only
ios,androidanddesktoptake effect;macosandlinuxare stored but not used.device_targeting.iosstring · uriFor iPhone and iPad visitors.
device_targeting.androidstring · uriFor Android visitors.
device_targeting.desktopstring · uriFor visitors not on a phone or tablet.
device_targeting.macosstring · uriStored, but not used when redirecting.
device_targeting.linuxstring · uriStored, but not used when redirecting.
og_previewOgPreviewThe title, description and image social apps show for the short link. Starter plan and up.
og_preview.og_titlestringThe preview title.
og_preview.og_descriptionstringThe preview description.
og_preview.og_image_urlstring · uriURL of the preview image.
qr_metadataQrMetadataSaved QR code style. Clikso does not generate QR images through the API.
qr_metadata.colorstringQR colour.
qr_metadata.shapestringQR dot shape.
qr_metadata.logo_urlstringURL of a logo for the QR centre.
qr_metadata.paramsobjectOther QR settings.
meta_pixel_idstring or nullMeta Pixel id: 6–20 digits. Starter plan and up.
Pattern
^\d{6,20}$tiktok_pixel_idstring or nullTikTok Pixel id: 8–40 letters and digits. Starter plan and up.
Pattern
^[A-Za-z0-9]{8,40}$ga4_measurement_idstring or nullGA4 measurement id, like
G-ABC123XYZ. Starter plan and up.Pattern
^G-[A-Z0-9]{4,16}$gtm_container_idstring or nullGoogle 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 · uuidrequiredThe link's id.
workspace_idstring · uuidrequiredThe workspace the link belongs to.
custom_domain_idstring · uuid or nullThe custom domain's id, or
nullon a Clikso domain.domainstringrequiredThe link's domain, like
clik.isorlinks.example.com.domain_statusstringrequireddefaulton a Clikso domain,activeon a live custom domain,deletedwhen the custom domain was removed and the link no longer works.One of:
default,active,deletedtitlestringrequiredA name for the link. Empty string when not set.
Up to 60 characters
descriptionstringrequiredA note about the link. Empty string when not set.
Up to 250 characters
short_codestringrequiredThe part after the domain in the short URL: 3–30 letters, digits and hyphens, and on Starter and up
/for a nested code likeacme/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 · urirequiredWhere the short link sends visitors.
statusstringrequiredactivelinks redirect.archivedandexpiredlinks do not.One of:
active,archived,expiredexpires_atstring · date-time or nullWhen the link stops working, or
nullfor never.expired_redirect_urlstring · uri or nullWhere visitors go after the link expires, or
nullfor Clikso's expired page.folder_idstring · uuid or nullThe folder the link is in, or
null.ab_testingarray of ABVariantA/B split destinations. An empty array when not set, never
null.ab_testing[].urlstring · urirequiredWhere this share of visitors goes.
ab_testing[].percentageintegerrequiredThis variant's weight. Weights are relative, so they do not have to add up to 100.
0–100
geo_targetingGeoTargeting or any or nullDestination per country, or
null.device_targetingDeviceTargeting or any or nullDestination per device, or
null.qr_metadataQrMetadata or any or nullSaved QR style, or
null.og_previewOgPreview or any or nullCustom social preview, or
null.meta_pixel_idstring or nullMeta Pixel id, or
null.tiktok_pixel_idstring or nullTikTok Pixel id, or
null.ga4_measurement_idstring or nullGoogle Analytics 4 measurement id, or
null.gtm_container_idstring or nullGoogle Tag Manager container id, or
null.has_passwordbooleanrequiredtruewhen visitors must enter a password. The password itself is never returned.click_countintegerrequiredLifetime clicks on this link.
Min 0
created_bystring · uuidThe user id of the member who created it.
created_atstring · date-timerequiredWhen the link was created.
updated_atstring · date-time or nullWhen the link last changed.
tagsarray of TagRefrequiredThe link's tags. An empty array when it has none.
tags[].idstring · uuidrequiredThe tag's id.
tags[].namestringrequiredThe tag's name.
Up to 10 characters
tags[].colorstring or nullrequiredThe tag's colour as
#rrggbb, or an empty string.creatorCreator or any or nullThe member who created the link, or
nullif unknown.
Common errors
UNAUTHORIZEDThe API key is missing, malformed or unknown.UPGRADE_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.FORBIDDENThe key only has Read permission, or its creator is a viewer in this workspace.NOT_FOUNDcustom_domain_id is not one of this workspace's domains.CONFLICTAnother link already uses this short_code on this domain, or it is a reserved name on a Clikso domain.VALIDATION_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.DESTINATION_BLOCKEDA URL in the body points at a link shortener or a blocked site. error.message says which one.USAGE_LIMIT_REACHEDYou have created your plan's monthly number of links.RATE_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_codemay 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_codemay also use/to make a nested code likeacme/launch(the short URL ishttps://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/gets402 UPGRADE_REQUIRED. - A bad
short_codegets422 VALIDATION_ERRORsaying what is wrong in plain English, for exampleshort_code: Use one slash (/) at a time, not two.— the same sentence is indetails.short_code. - On
clik.is(and the other Clikso domains) some official-sounding names are reserved, such aspricingorlogin, 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. domainonly acceptsclik.isor one of Clikso's free domains (see List free Clikso domains). For your own domain, send its id ascustom_domain_id; it must be verified.- Keep
titleto 60 characters anddescriptionto 250.passwordcan be up to 100 characters and is never returned — you only seehas_password. expires_atmust be a date (ISO 8601, for example2026-12-31T23:59:00Z) andexpired_redirect_urlat most 2048 characters. A bad value gets422 VALIDATION_ERRORwithdetails.fieldnaming 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 withDESTINATION_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,androidanddesktopdevice rules take effect.
