Links API
Create and manage short links programmatically.
Create Link
Create a new short link.
POST /v1/links
This endpoint is limited to 100 requests per minute per token. See Rate Limits.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
teamId | number | No | Create the link in this team instead of your personal workspace. See Team Workspaces |
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | The destination URL. Must be an http or https URL |
slug | string | No | Custom slug, 3–50 letters, numbers or hyphens. If you leave it out, 8 random characters are used. It can't match another short link, a bio profile or a reserved path such as dashboard |
title | string | No | Link title, up to 200 characters |
startsAt | string | No | When the link starts working, as an ISO 8601 UTC date with seconds, for example 2024-02-01T00:00:00Z. Time zone offsets such as +03:00 are rejected |
expiresAt | string | No | When the link stops working, in the same format as startsAt |
clickLimit | integer | No | The link stops working after this many clicks. Must be a positive whole number |
expirationUrl | string | No | Where visitors are sent while the link isn't working (before startsAt, after expiresAt, or once clickLimit is reached). Must be an http or https URL |
utmSource | string | No | Added to the destination as utm_source |
utmMedium | string | No | Added to the destination as utm_medium |
utmCampaign | string | No | Added to the destination as utm_campaign |
appLinkingEnabled | boolean | No | Open the destination in its app on mobile, when the destination is a supported app. Default false |
iosEnabled | boolean | No | Use App Linking on iOS. Default true |
androidEnabled | boolean | No | Use App Linking on Android. Default true |
redirectDelayEnabled | boolean | No | Show a page with the link title before redirecting. Its "Continue" button becomes active after 5 seconds. Default false |
startsAt, expiresAt, clickLimit and expirationUrl also accept null. Every new destination is checked against malicious URL lists, and a flagged URL is rejected with BLOCKED_URL.
Example Usage
interface CreateLinkRequest {
url: string;
slug?: string;
title?: string;
startsAt?: string | null;
expiresAt?: string | null;
clickLimit?: number | null;
expirationUrl?: string | null;
utmSource?: string;
utmMedium?: string;
utmCampaign?: string;
appLinkingEnabled?: boolean;
iosEnabled?: boolean;
androidEnabled?: boolean;
redirectDelayEnabled?: boolean;
}
interface LinkResponse {
id: number;
url: string;
slug: string;
shortUrl: string;
title: string | null;
clicks: number;
createdAt: string;
startsAt: string | null;
expiresAt: string | null;
clickLimit: number | null;
expirationUrl: string | null;
utmSource: string | null;
utmMedium: string | null;
utmCampaign: string | null;
appLinkingEnabled: boolean;
iosEnabled: boolean;
androidEnabled: boolean;
redirectDelayEnabled: boolean;
}
interface ApiResponse<T> {
success: boolean;
data: T;
}
<script setup lang="ts">
const createLink = async () => {
const response = await fetch("https://linked.is/api/v1/links", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://example.com/my-long-url",
slug: "my-link",
title: "My Example Link",
startsAt: "2024-02-01T00:00:00Z",
clickLimit: 1000,
utmSource: "newsletter",
}),
});
const data = await response.json();
console.log(data);
return data;
};
</script>
<template>
<div>
<button @click="createLink">Create Link</button>
</div>
</template>
import requests
url = "https://linked.is/api/v1/links"
headers = {
"Authorization": "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
}
payload = {
"url": "https://example.com/my-long-url",
"slug": "my-link",
"title": "My Example Link",
"startsAt": "2024-02-01T00:00:00Z",
"clickLimit": 1000,
"utmSource": "newsletter"
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
curl -X POST "https://linked.is/api/v1/links" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/my-long-url",
"slug": "my-link",
"title": "My Example Link",
"startsAt": "2024-02-01T00:00:00Z",
"clickLimit": 1000,
"utmSource": "newsletter"
}'
Response Example
<script setup lang="ts">
const linkResponse = {
success: true,
data: {
id: 42,
url: "https://example.com/my-long-url",
slug: "my-link",
shortUrl: "https://linked.is/my-link",
title: "My Example Link",
clicks: 0,
createdAt: "2024-01-15T10:30:00.000Z",
startsAt: "2024-02-01T00:00:00.000Z",
expiresAt: null,
clickLimit: 1000,
expirationUrl: null,
utmSource: "newsletter",
utmMedium: null,
utmCampaign: null,
appLinkingEnabled: false,
iosEnabled: true,
androidEnabled: true,
redirectDelayEnabled: false,
},
};
</script>
<template>
<pre>{{ JSON.stringify(linkResponse, null, 2) }}</pre>
</template>
Get Link
Retrieve a specific link by ID. Works for your personal links and for links of teams you are a member of.
GET /v1/links/:id
Example Usage
<script setup lang="ts">
const getLink = async () => {
const response = await fetch("https://linked.is/api/v1/links/42", {
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
},
});
const data = await response.json();
console.log(data);
return data;
};
</script>
<template>
<div>
<button @click="getLink">Get Link</button>
</div>
</template>
import requests
url = "https://linked.is/api/v1/links/42"
headers = {
"Authorization": "Bearer YOUR_API_TOKEN"
}
response = requests.get(url, headers=headers)
print(response.json())
curl -X GET "https://linked.is/api/v1/links/42" \
-H "Authorization: Bearer YOUR_API_TOKEN"
Response Example
This endpoint returns the basic fields of the link. The scheduling, UTM and App Linking fields are returned by Create Link and Update Link.
<script setup lang="ts">
const linkResponse = {
success: true,
data: {
id: 42,
url: "https://example.com/my-long-url",
slug: "my-link",
title: "My Example Link",
shortUrl: "https://linked.is/my-link",
clicks: 42,
createdAt: "2024-01-15T10:30:00.000Z",
updatedAt: "2024-01-15T10:30:00.000Z",
},
};
</script>
<template>
<pre>{{ JSON.stringify(linkResponse, null, 2) }}</pre>
</template>
Update Link
Update an existing link. Send only the fields you want to change.
PATCH /v1/links/:id
Request Body
| Parameter | Type | Description |
|---|---|---|
url | string | The destination URL (http or https) |
slug | string | Custom slug, 3–50 letters, numbers or hyphens. Must be free, as in Create Link |
title | string | Link title, up to 200 characters |
startsAt | string | null | Start date (ISO 8601 UTC, for example 2024-02-01T00:00:00Z). null removes it |
expiresAt | string | null | Expiration date (ISO 8601 UTC). null removes it |
clickLimit | integer | null | Maximum number of clicks allowed. null removes it |
expirationUrl | string | null | Where visitors are sent while the link isn't working. null removes it |
utmSource | string | UTM source parameter |
utmMedium | string | UTM medium parameter |
utmCampaign | string | UTM campaign parameter |
appLinkingEnabled | boolean | Open the destination in its app on mobile |
iosEnabled | boolean | Use App Linking on iOS |
androidEnabled | boolean | Use App Linking on Android |
redirectDelayEnabled | boolean | Show a page with the link title before redirecting (the "Continue" button becomes active after 5 seconds) |
title and the UTM fields don't accept null. To clear them, send an empty string. A new url or expirationUrl is checked against malicious URL lists, like on create.
Example Usage
<script setup lang="ts">
const updateLink = async () => {
const response = await fetch("https://linked.is/api/v1/links/42", {
method: "PATCH",
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Updated Title",
clickLimit: 5000,
utmCampaign: "summer_sale",
}),
});
const data = await response.json();
console.log(data);
return data;
};
</script>
<template>
<div>
<button @click="updateLink">Update Link</button>
</div>
</template>
import requests
url = "https://linked.is/api/v1/links/42"
headers = {
"Authorization": "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
}
payload = {
"title": "Updated Title",
"clickLimit": 5000,
"utmCampaign": "summer_sale"
}
response = requests.patch(url, json=payload, headers=headers)
print(response.json())
curl -X PATCH "https://linked.is/api/v1/links/42" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Updated Title",
"clickLimit": 5000,
"utmCampaign": "summer_sale"
}'
Response Example
<script setup lang="ts">
const updateResponse = {
success: true,
data: {
id: 42,
url: "https://example.com/my-long-url",
slug: "my-link",
shortUrl: "https://linked.is/my-link",
title: "Updated Title",
clicks: 42,
createdAt: "2024-01-15T10:30:00.000Z",
updatedAt: "2024-01-16T14:20:00.000Z",
startsAt: null,
expiresAt: null,
clickLimit: 5000,
expirationUrl: null,
utmSource: null,
utmMedium: null,
utmCampaign: "summer_sale",
appLinkingEnabled: false,
iosEnabled: true,
androidEnabled: true,
redirectDelayEnabled: false,
},
};
</script>
<template>
<pre>{{ JSON.stringify(updateResponse, null, 2) }}</pre>
</template>
Delete Link
Delete a link permanently. Its click history is deleted with it.
DELETE /v1/links/:id
Example Usage
<script setup lang="ts">
const deleteLink = async () => {
const response = await fetch("https://linked.is/api/v1/links/42", {
method: "DELETE",
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
},
});
const data = await response.json();
console.log(data);
return data;
};
</script>
<template>
<div>
<button @click="deleteLink" class="danger">Delete Link</button>
</div>
</template>
import requests
url = "https://linked.is/api/v1/links/42"
headers = {
"Authorization": "Bearer YOUR_API_TOKEN"
}
response = requests.delete(url, headers=headers)
print(response.json())
curl -X DELETE "https://linked.is/api/v1/links/42" \
-H "Authorization: Bearer YOUR_API_TOKEN"
Response Example
<script setup lang="ts">
const deleteResponse = {
success: true,
data: {
deleted: true,
},
};
</script>
<template>
<pre>{{ JSON.stringify(deleteResponse, null, 2) }}</pre>
</template>
List Links
Get a paginated list of the links in your personal workspace, or in a team with teamId.
GET /v1/links
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number |
limit | number | 20 | Items per page (1–100) |
search | string | - | Search by URL, title or slug (contains, case-insensitive for English letters). Only the first 48 bytes of the search text are used |
sort | string | createdAt | Sort field: createdAt or clicks. Any other value sorts by createdAt |
order | string | desc | Sort order (asc or desc) |
teamId | number | - | List the links of this team. See Team Workspaces |
Example Usage
<script setup lang="ts">
const listLinks = async () => {
const response = await fetch(
"https://linked.is/api/v1/links?page=1&limit=10&sort=clicks&order=desc",
{
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
},
},
);
const data = await response.json();
console.log(data);
return data;
};
</script>
<template>
<div>
<button @click="listLinks">List Links</button>
</div>
</template>
import requests
url = "https://linked.is/api/v1/links"
params = {
"page": 1,
"limit": 10,
"sort": "clicks",
"order": "desc"
}
headers = {
"Authorization": "Bearer YOUR_API_TOKEN"
}
response = requests.get(url, params=params, headers=headers)
print(response.json())
curl -X GET "https://linked.is/api/v1/links?page=1&limit=10&sort=clicks&order=desc" \
-H "Authorization: Bearer YOUR_API_TOKEN"
Response Example
<script setup lang="ts">
const listResponse = {
success: true,
data: {
items: [
{
id: 42,
url: "https://example.com/my-long-url",
slug: "my-link",
title: "My Example Link",
clicks: 42,
createdAt: "2024-01-15T10:30:00.000Z",
updatedAt: "2024-01-16T14:20:00.000Z",
shortUrl: "https://linked.is/my-link",
},
],
pagination: {
page: 1,
limit: 10,
total: 25,
totalPages: 3,
},
},
};
</script>
<template>
<pre>{{ JSON.stringify(listResponse, null, 2) }}</pre>
</template>
Get QR Code
Generate a QR code image for a short link. The QR code encodes the link's shortUrl.
GET /v1/links/:id/qr
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
size | number | 256 | Image width in pixels. Values outside 100–1000 are clamped to that range |
format | string | png | Output format: png, svg, or base64. Any other value returns a PNG |
margin | number | 2 | Quiet zone margin in modules, 1–10. 0 is treated as the default 2 |
dark | string | #000000 | Dark/foreground color as a hex code (#RRGGBB or #RRGGBBAA) |
light | string | #ffffff | Light/background color as a hex code |
Encode # as %23 in the query string.
Example Usage
<script setup lang="ts">
const getQrCodeBase64 = async () => {
const response = await fetch(
"https://linked.is/api/v1/links/42/qr?format=base64",
{
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
},
},
);
const { data } = await response.json();
console.log(data.qrCode); // data:image/png;base64,...
return data;
};
</script>
<template>
<div>
<button @click="getQrCodeBase64">Get QR Code (Base64)</button>
</div>
</template>
import requests
url = "https://linked.is/api/v1/links/42/qr"
params = {"size": 512, "format": "png"}
headers = {"Authorization": "Bearer YOUR_API_TOKEN"}
response = requests.get(url, params=params, headers=headers)
with open("qrcode.png", "wb") as f:
f.write(response.content)
# Save as image file
curl -X GET "https://linked.is/api/v1/links/42/qr?size=512" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
--output qrcode.png
# Get Base64 JSON
curl -X GET "https://linked.is/api/v1/links/42/qr?format=base64" \
-H "Authorization: Bearer YOUR_API_TOKEN"
Response Examples
<script setup lang="ts">
const qrResponse = {
success: true,
data: {
url: "https://linked.is/my-link",
qrCode: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
format: "base64",
size: 256,
},
};
</script>
<template>
<pre>{{ JSON.stringify(qrResponse, null, 2) }}</pre>
</template>
The response will be the raw image with the matching Content-Type header (image/png or image/svg+xml).
Custom Colors Example
# Red QR code on white background
curl -X GET "https://linked.is/api/v1/links/42/qr?dark=%23ff0000&light=%23ffffff" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
--output qrcode.png
<script setup lang="ts">
const generateCustomQR = async () => {
// This would generate a red QR code on white background
const url =
"https://linked.is/api/v1/links/42/qr?dark=%23ff0000&light=%23ffffff";
console.log("Custom QR URL:", url);
return url;
};
</script>
<template>
<div>
<button @click="generateCustomQR">Generate Custom QR</button>
</div>
</template>
png and svg formats, the response is the raw image data with the matching Content-Type header, not JSON. For base64 format, the response is a JSON object containing the data URL.Error Codes
| Code | Description |
|---|---|
INVALID_REQUEST | The link ID in the path is not a number |
LINK_NOT_FOUND | The link doesn't exist or you don't have access to it |
SLUG_TAKEN | The slug is already used by a link or a bio profile |
SLUG_RESERVED | The slug is a reserved path (for example dashboard or api) |
BLOCKED_URL | The destination or expiration URL is flagged as malicious (on create and update) |
CREATE_FAILED | The link couldn't be created |
UPDATE_FAILED | The link couldn't be updated |
An invalid body, for example a url that isn't an http or https URL or a slug with other characters, fails with a 400 validation error. See Errors.