API reference / Links
Update a link
https://app.clik.so/api /links/{id}In short
null to clear a field, for example expires_at to remove the expiry or geo_targeting to remove the country rules.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 PUT "https://app.clik.so/api/links/3f6c1a2e-9b4d-4e8f-a1c2-7d9e0f1a2b3c" \
-H "Authorization: Bearer $CLIKSO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination_url": "https://example.com/autumn-sale",
"title": "Autumn sale",
"expires_at": null
}'const res = await fetch("https://app.clik.so/api/links/3f6c1a2e-9b4d-4e8f-a1c2-7d9e0f1a2b3c", {
method: "PUT",
headers: {
Authorization: `Bearer ${process.env.CLIKSO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"destination_url": "https://example.com/autumn-sale",
"title": "Autumn sale",
"expires_at": null
}),
});
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.put(
"https://app.clik.so/api/links/3f6c1a2e-9b4d-4e8f-a1c2-7d9e0f1a2b3c",
headers={"Authorization": f"Bearer {os.environ['CLIKSO_API_KEY']}"},
json={
"destination_url": "https://example.com/autumn-sale",
"title": "Autumn sale",
"expires_at": None,
},
)
print(response.status_code, response.json())Uses requests. Set CLIKSO_API_KEY first.
<?php
$body = <<<'JSON'
{
"destination_url": "https://example.com/autumn-sale",
"title": "Autumn sale",
"expires_at": null
}
JSON;
$ch = curl_init('https://app.clik.so/api/links/3f6c1a2e-9b4d-4e8f-a1c2-7d9e0f1a2b3c');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PUT',
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/autumn-sale",
"title": "Autumn sale",
"expires_at": null
}`)
req, err := http.NewRequest("PUT", "https://app.clik.so/api/links/3f6c1a2e-9b4d-4e8f-a1c2-7d9e0f1a2b3c", 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/3f6c1a2e-9b4d-4e8f-a1c2-7d9e0f1a2b3c')
request = Net::HTTP::Put.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("CLIKSO_API_KEY")}"
request["Content-Type"] = "application/json"
request.body = {
'destination_url' => 'https://example.com/autumn-sale',
'title' => 'Autumn sale',
'expires_at' => nil
}.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/autumn-sale",
"title": "Autumn sale",
"expires_at": null
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://app.clik.so/api/links/3f6c1a2e-9b4d-4e8f-a1c2-7d9e0f1a2b3c"))
.header("Authorization", "Bearer " + System.getenv("CLIKSO_API_KEY"))
.header("Content-Type", "application/json")
.method("PUT", 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("PUT"), "https://app.clik.so/api/links/3f6c1a2e-9b4d-4e8f-a1c2-7d9e0f1a2b3c");
request.Headers.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("CLIKSO_API_KEY"));
request.Content = new StringContent(
"""
{
"destination_url": "https://example.com/autumn-sale",
"title": "Autumn sale",
"expires_at": null
}
""",
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/autumn-sale",
"title": "Autumn sale",
"expires_at": null
})json";
std::string response;
curl_easy_setopt(curl, CURLOPT_URL, "https://app.clik.so/api/links/3f6c1a2e-9b4d-4e8f-a1c2-7d9e0f1a2b3c");
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
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": 200,
"success": true,
"message": "Link updated",
"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": "Autumn sale",
"description": "",
"short_code": "summer-sale",
"destination_url": "https://example.com/autumn-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": 128,
"created_by": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
"created_at": "2026-10-09T09:30:00.000Z",
"updated_at": "2026-10-10T08:15: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": ""
}
}
}Path parameters
idstring · uuidrequiredThe link's id.
Request body
destination_urlstring · uriA new destination.
short_codestringA new code, by the same rules as on create (3–30 characters;
/for a nested code on Starter and up,402 UPGRADE_REQUIREDon Free). A blank value, or the current code, keeps the current one.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 or nullMove the link to
clik.isor another free Clikso domain.custom_domain_idstring · uuid or nullMove the link to one of your verified custom domains.
titlestring or nullA new name.
nullclears it.Up to 60 characters
descriptionstring or nullA new note.
nullclears it.Up to 250 characters
statusstringarchivedorexpiredturns the short URL off;activeturns it back on.One of:
active,archived,expiredfolder_idstring · uuid or nullA new folder.
nulltakes the link out of its folder.tagsarray of string · uuid or nullReplaces all tags.
[]removes them all;nullor leaving it out keeps them.passwordstring or nullA new password.
nullremoves it.Up to 100 characters
expires_atstring · date-time or nullA new expiry.
nullremoves it.expired_redirect_urlstring · uri or nullA new after-expiry destination.
nullclears it.ab_testingarray of ABVariant or nullNew A/B variants.
nullor[]removes them.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 nullNew country rules.
nullor{}removes them.device_targetingDeviceTargeting or any or nullNew device rules.
nullor{}removes them.og_previewOgPreview or any or nullNew social preview.
nullor{}removes it.qr_metadataQrMetadata or any or nullNew QR style.
nullor{}resets it.meta_pixel_idstring or nullNew Meta Pixel id.
nullclears it.Pattern
^\d{6,20}$tiktok_pixel_idstring or nullNew TikTok Pixel id.
nullclears it.Pattern
^[A-Za-z0-9]{8,40}$ga4_measurement_idstring or nullNew GA4 measurement id.
nullclears it.Pattern
^G-[A-Z0-9]{4,16}$gtm_container_idstring or nullNew Tag Manager container id.
nullclears it.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 ·
passwordOnly a new, non-empty value. Keeping or clearing the current one is always allowed.Starter and up - Custom social preview ·
og_previewOnly a new, non-empty value. Keeping or clearing the current one is always allowed.Starter and up - Tracking pixels ·
meta_pixel_id,tiktok_pixel_id,ga4_measurement_id,gtm_container_idOnly a new, non-empty value.Starter and up - Geo-targeting ·
geo_targetingOnly a new, non-empty value. Keeping or clearing the current one is always allowed.Pro and up - Device targeting ·
device_targetingOnly a new, non-empty value. Keeping or clearing the current one is always allowed.Pro and up - A/B testing ·
ab_testingOnly a new, non-empty value. Keeping or clearing the current one is always allowed.Pro and up - nestedSlugs ·
short_codeOnly a new code with a `/` in it. Re-sending the link's current code, or changing to a code without `/`, is always allowed.Starter and up
What you get back
200 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 new value on a field your plan does not include, including a new short_code with a / on Free.FORBIDDENThe key only has Read permission, or its creator is a viewer in this workspace.NOT_FOUNDNo live link with this id, or custom_domain_id is not one of this workspace's domains.CONFLICTThe new short_code is already taken on that domain, or it is a reserved name on a Clikso domain.VALIDATION_ERRORA field is malformed or too long (details.field names it; a bad short_code is named in details.short_code), 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.RATE_LIMITEDThe key went over its per-minute request limit.Every code and what to do about it: Errors.
Good to know
- This is
PUT, notPATCH, but it behaves like a patch: fields you leave out keep their value. nullclears a plain field:password: nullremoves the password,folder_id: nulltakes the link out of its folder,expires_at: nullremoves the expiry.nullalso clears the rule objects:geo_targeting,device_targeting,ab_testing,og_previewandqr_metadata(since 1.61.1; before thatnullwas ignored for them).{}(or[]forab_testing) still works too. Leaving a field out keeps it.tagsreplaces the whole tag list.tags: []removes every tag, andtags: null(or leaving it out) keeps the tags as they are.- You can change
short_codeanddestination_urlafter creation. The old short URL stops working at once when the code changes.short_codefollows the same rules as on create: 3 to 30 characters, and/for a nested code on Starter and up. - Setting
statustoarchivedorexpiredstops the short URL from working. Setting it back toactiveturns it on again. - Keep
titleto 60 characters anddescriptionto 250,expires_ata valid date andexpired_redirect_urlat most 2048 characters, or you get422withdetails.fieldnaming the field. - If your plan was downgraded, a link keeps the paid settings it already has, and you can still edit its other fields. That includes a nested
short_code: the link keeps working and you can send its current code back unchanged, but you can't change it to a different code with/.
