Skip to content

Links API

API endpoints for managing short links

Create and manage short links programmatically.

Create a new short link.

POST /v1/links

This endpoint is limited to 100 requests per minute per token. See Rate Limits.

Query Parameters

ParameterTypeRequiredDescription
teamIdnumberNoCreate the link in this team instead of your personal workspace. See Team Workspaces

Request Body

ParameterTypeRequiredDescription
urlstringYesThe destination URL. Must be an http or https URL
slugstringNoCustom 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
titlestringNoLink title, up to 200 characters
startsAtstringNoWhen 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
expiresAtstringNoWhen the link stops working, in the same format as startsAt
clickLimitintegerNoThe link stops working after this many clicks. Must be a positive whole number
expirationUrlstringNoWhere 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
utmSourcestringNoAdded to the destination as utm_source
utmMediumstringNoAdded to the destination as utm_medium
utmCampaignstringNoAdded to the destination as utm_campaign
appLinkingEnabledbooleanNoOpen the destination in its app on mobile, when the destination is a supported app. Default false
iosEnabledbooleanNoUse App Linking on iOS. Default true
androidEnabledbooleanNoUse App Linking on Android. Default true
redirectDelayEnabledbooleanNoShow 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>

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>

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>

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 an existing link. Send only the fields you want to change.

PATCH /v1/links/:id

Request Body

ParameterTypeDescription
urlstringThe destination URL (http or https)
slugstringCustom slug, 3–50 letters, numbers or hyphens. Must be free, as in Create Link
titlestringLink title, up to 200 characters
startsAtstring | nullStart date (ISO 8601 UTC, for example 2024-02-01T00:00:00Z). null removes it
expiresAtstring | nullExpiration date (ISO 8601 UTC). null removes it
clickLimitinteger | nullMaximum number of clicks allowed. null removes it
expirationUrlstring | nullWhere visitors are sent while the link isn't working. null removes it
utmSourcestringUTM source parameter
utmMediumstringUTM medium parameter
utmCampaignstringUTM campaign parameter
appLinkingEnabledbooleanOpen the destination in its app on mobile
iosEnabledbooleanUse App Linking on iOS
androidEnabledbooleanUse App Linking on Android
redirectDelayEnabledbooleanShow 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>

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

Response Example

<script setup lang="ts">
const deleteResponse = {
  success: true,
  data: {
    deleted: true,
  },
};
</script>

<template>
  <pre>{{ JSON.stringify(deleteResponse, null, 2) }}</pre>
</template>

Get a paginated list of the links in your personal workspace, or in a team with teamId.

GET /v1/links

Query Parameters

ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber20Items per page (1–100)
searchstring-Search by URL, title or slug (contains, case-insensitive for English letters). Only the first 48 bytes of the search text are used
sortstringcreatedAtSort field: createdAt or clicks. Any other value sorts by createdAt
orderstringdescSort order (asc or desc)
teamIdnumber-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>

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

ParameterTypeDefaultDescription
sizenumber256Image width in pixels. Values outside 100–1000 are clamped to that range
formatstringpngOutput format: png, svg, or base64. Any other value returns a PNG
marginnumber2Quiet zone margin in modules, 1–10. 0 is treated as the default 2
darkstring#000000Dark/foreground color as a hex code (#RRGGBB or #RRGGBBAA)
lightstring#ffffffLight/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>

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>

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>
Note: For 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

CodeDescription
INVALID_REQUESTThe link ID in the path is not a number
LINK_NOT_FOUNDThe link doesn't exist or you don't have access to it
SLUG_TAKENThe slug is already used by a link or a bio profile
SLUG_RESERVEDThe slug is a reserved path (for example dashboard or api)
BLOCKED_URLThe destination or expiration URL is flagged as malicious (on create and update)
CREATE_FAILEDThe link couldn't be created
UPDATE_FAILEDThe 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.