Skip to content
API Reference

LLM Integration

How LLMs and AI agents can use the linked.is API

The linked.is API is designed to be easily consumed by Large Language Models (LLMs) and AI agents. This guide explains how to integrate our API with AI-powered tools.

Quick Start for LLMs

Every page of this API reference is also available as plain Markdown. Add /raw in front of the docs path and .md at the end:

https://linked.is/raw/docs/api.md
https://linked.is/raw/docs/api/authentication.md
https://linked.is/raw/docs/api/links.md
https://linked.is/raw/docs/api/profiles.md
https://linked.is/raw/docs/api/analytics.md
https://linked.is/raw/docs/api/rate-limits.md

These files contain the endpoints, request and response formats, and examples in a format that is easy for LLMs to read.

Tip: Give an LLM the Markdown pages it needs before it makes requests, so it knows the exact field names and limits.

Common LLM Use Cases

1. URL Shortening

LLMs can shorten URLs on behalf of users:

<script setup lang="ts">
const shortenUrl = async () => {
  // Shorten a URL
  const response = await fetch('https://linked.is/api/v1/links', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      url: 'https://example.com/very/long/article/path'
    })
  })

  const { data } = await response.json()
  // Returns: { id: 42, shortUrl: "https://linked.is/aB3dE9xZ", ... }
  console.log(data);
  return data;
};
</script>

<template>
  <div>
    <button @click="shortenUrl">Shorten URL</button>
  </div>
</template>

2. Analytics Retrieval

Query link performance data:

<script setup lang="ts">
const getAnalytics = async () => {
  // Get analytics overview
  const response = await fetch('https://linked.is/api/v1/analytics/overview', {
    headers: { 'Authorization': `Bearer ${API_TOKEN}` }
  })

  const { data } = await response.json()
  // Returns: { totalClicks: 1500, totalLinks: 25, topLinks: [...], ... }
  console.log(data);
  return data;
};
</script>

<template>
  <div>
    <button @click="getAnalytics">Get Analytics</button>
  </div>
</template>

3. Profile Management

Create and manage link-in-bio profiles:

<script setup lang="ts">
const createProfile = async () => {
  // Create a new bio profile
  const response = await fetch('https://linked.is/api/v1/profiles', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      displayName: 'AI Assistant',
      slug: 'ai-assistant',
      bio: 'Links curated by AI'
    })
  })

  const data = await response.json();
  console.log(data);
  return data;
};
</script>

<template>
  <div>
    <button @click="createProfile">Create AI Profile</button>
  </div>
</template>

API Response Handling

Successful responses have success: true and the result in data. Errors carry the error code in data.error, or only a message for authentication, validation and rate limit errors. See Errors.

<script setup lang="ts">
const apiResponse = {
  success: true,
  data: { /* ... */ }
};

const handleApiResponse = (result: any) => {
  if (result.success) {
    // Process data
    const links = result.data.items;
    console.log("Links:", links);
  } else {
    // Handle error
    const errorCode = result.data?.error?.code;
    const errorMessage = result.data?.error?.message || result.message;
    console.error("Error:", result.statusCode, errorCode, errorMessage);
  }
};
</script>

<template>
  <div>
    <h3>API Response:</h3>
    <pre>{{ JSON.stringify(apiResponse, null, 2) }}</pre>
    <button @click="handleApiResponse(apiResponse)">Process Response</button>
  </div>
</template>

Error Handling

Common errors LLMs should handle:

ErrorMeaningSuggested Action
400 with statusMessage: "Validation Error"A field is missing or invalid, for example a URL that isn't http or httpsRead message, fix the field and retry
BLOCKED_URLThe URL is flagged as maliciousDon't retry; tell the user the URL was blocked
SLUG_TAKENThe slug is used by another link or a profileGenerate a different slug
SLUG_RESERVEDThe slug is a reserved pathGenerate a different slug
LINK_NOT_FOUNDLink doesn't exist or isn't accessibleInform user the link was not found
PROFILE_NOT_FOUNDProfile doesn't exist or isn't accessibleSuggest creating a new profile
401Token is missing, invalid or expiredAsk the user for a valid API token
429Rate limit exceededWait for the Retry-After seconds, then retry

Best Practices for AI Agents

1. Rate Limiting

Respect rate limits to avoid being blocked. Creating links is limited to 100 requests per minute per token:

<script setup lang="ts">
const makeRequestWithRetry = async (url: string, options: RequestInit, maxRetries = 3) => {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fetch(url, options);
    
    if (response.status === 429) {  // Rate limited
      const retryAfter = parseInt(response.headers.get('Retry-After') || '60', 10);
      await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
      continue;
    }
    
    return response.json();
  }
  
  throw new Error('Max retries exceeded');
};

const handleRetryRequest = async () => {
  try {
    const result = await makeRequestWithRetry(
      "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" })
      }
    );
    console.log("Result:", result);
  } catch (error) {
    console.error("Max retries exceeded:", error);
  }
};
</script>

<template>
  <div>
    <button @click="handleRetryRequest">Retry Request</button>
  </div>
</template>

2. Batch Operations

When processing multiple items, use pagination efficiently:

<script setup lang="ts">
const getAllLinks = async (apiToken: string) => {
  const allLinks = [];
  let page = 1;
  
  while (true) {
    const response = await fetch(
      `https://linked.is/api/v1/links?page=${page}&limit=100`,
      {
        headers: {
          "Authorization": `Bearer ${apiToken}`
        }
      }
    );
    
    const result = await response.json();
    const data = result.data;
    allLinks.push(...data.items);
    
    if (page >= data.pagination.totalPages) {
      break;
    }
    page += 1;
  }
  
  return allLinks;
};

const fetchAllLinks = async () => {
  try {
    const links = await getAllLinks("YOUR_API_TOKEN");
    console.log("All links:", links);
  } catch (error) {
    console.error("Error fetching links:", error);
  }
};
</script>

<template>
  <div>
    <button @click="fetchAllLinks">Fetch All Links</button>
  </div>
</template>

3. Idempotent Operations

Use custom slugs for idempotent link creation. SLUG_TAKEN means the slug is in use, but not necessarily by you: another account or a bio profile may own it. Check your own links before reusing the result:

<script setup lang="ts">
const createOrGetLink = async (url: string, slug: string, apiToken: string) => {
  const headers = {
    'Authorization': `Bearer ${apiToken}`,
    'Content-Type': 'application/json'
  };

  // Try to create with specific slug
  const response = await fetch(
    'https://linked.is/api/v1/links',
    {
      method: 'POST',
      headers,
      body: JSON.stringify({url, slug})
    }
  );
  
  const result = await response.json();
  
  if (result.data?.error?.code === 'SLUG_TAKEN') {
    // Look for the slug among your own links
    const search = await fetch(
      `https://linked.is/api/v1/links?search=${encodeURIComponent(slug)}`,
      { headers }
    );
    const { data } = await search.json();
    const existing = data.items.find((link: { slug: string }) => link.slug === slug);
    if (existing) return existing;
    throw new Error('Slug is used by someone else, choose another one');
  }
  
  return result.data;
};

const handleLinkCreation = async () => {
  try {
    const link = await createOrGetLink(
      "https://example.com/article",
      "my-slug",
      "YOUR_API_TOKEN"
    );
    console.log("Link created or retrieved:", link);
  } catch (error) {
    console.error("Error:", error);
  }
};
</script>

<template>
  <div>
    <button @click="handleLinkCreation">Create/Get Link</button>
  </div>
</template>

Function Calling Schema

For LLMs that support function calling, here's the schema:

<script setup lang="ts">
const createShortLinkSchema = {
  name: "create_short_link",
  description: "Create a shortened URL using linked.is",
  parameters: {
    type: "object",
    properties: {
      url: {
        type: "string",
        description: "The long URL to shorten (http or https)"
      },
      slug: {
        type: "string",
        description: "Optional custom slug for the short URL: 3-50 letters, numbers or hyphens"
      }
    },
    required: ["url"]
  }
};

const getLinkAnalyticsSchema = {
  name: "get_link_analytics",
  description: "Get click analytics for a short link",
  parameters: {
    type: "object",
    properties: {
      link_id: {
        type: "integer",
        description: "The ID of the link"
      },
      start: {
        type: "string",
        description: "Optional start date (ISO 8601), defaults to 30 days ago"
      },
      end: {
        type: "string",
        description: "Optional end date (ISO 8601), defaults to now"
      }
    },
    required: ["link_id"]
  }
};
</script>

<template>
  <div>
    <h3>Create Short Link Schema:</h3>
    <pre>{{ JSON.stringify(createShortLinkSchema, null, 2) }}</pre>
    <h3>Get Link Analytics Schema:</h3>
    <pre>{{ JSON.stringify(getLinkAnalyticsSchema, null, 2) }}</pre>
  </div>
</template>

Your code runs the function: create_short_link maps to POST /v1/links and get_link_analytics to GET /v1/links/:id/stats.

MCP Integration

For AI assistants using Model Context Protocol (MCP), you can expose linked.is as a tool in your own MCP server. Define the tool, and have its handler call POST /v1/links with your API token:

<script setup lang="ts">
const mcpSchema = {
  tools: [
    {
      name: "linkedis_shorten",
      description: "Shorten a URL using linked.is",
      inputSchema: {
        type: "object",
        properties: {
          url: { type: "string" }
        },
        required: ["url"]
      }
    }
  ]
};
</script>

<template>
  <div>
    <h3>MCP Integration Schema:</h3>
    <pre>{{ JSON.stringify(mcpSchema, null, 2) }}</pre>
  </div>
</template>

Security Considerations

Important for AI Developers:
  • Store API tokens securely, never in code
  • Use environment variables for token storage
  • Call the API from your server, not from the browser
  • Implement token rotation policies (tokens expire after one year)
  • Log API usage for audit purposes

Here's a complete example of an AI agent managing links:

<script setup lang="ts">
interface LinkData {
  id: number;
  url: string;
  slug: string;
  shortUrl: string;
  title: string | null;
  clicks: number;
  createdAt: string;
}

interface StatsData {
  totalClicks: number;
  countries: Array<{ country: string; code: string | null; clicks: number }>;
  devices: Array<{ device: string; clicks: number }>;
  browsers: Array<{ browser: string; clicks: number }>;
  referrers: Array<{ referrer: string; clicks: number }>;
}

interface LinkedisAgent {
  apiToken: string;
  baseUrl: string;
  headers: Record<string, string>;
}

const createAgent = (apiToken: string): LinkedisAgent => {
  return {
    apiToken,
    baseUrl: 'https://linked.is/api/v1',
    headers: {
      'Authorization': `Bearer ${apiToken}`,
      'Content-Type': 'application/json'
    }
  };
};

// Create a short link
const shorten = async (agent: LinkedisAgent, url: string, slug?: string): Promise<LinkData> => {
  const payload: { url: string; slug?: string } = { url };
  if (slug) {
    payload.slug = slug;
  }
  
  const response = await fetch(
    `${agent.baseUrl}/links`,
    {
      method: 'POST',
      headers: agent.headers,
      body: JSON.stringify(payload)
    }
  );
  
  const result = await response.json();
  return result.data;
};

// Get link statistics
const getStats = async (agent: LinkedisAgent, linkId: number): Promise<StatsData> => {
  const response = await fetch(
    `${agent.baseUrl}/links/${linkId}/stats`,
    {
      headers: agent.headers
    }
  );
  
  const result = await response.json();
  return result.data;
};

// Search links by URL, title or slug
const searchLinks = async (agent: LinkedisAgent, query: string): Promise<LinkData[]> => {
  const response = await fetch(
    `${agent.baseUrl}/links?search=${encodeURIComponent(query)}`,
    {
      headers: agent.headers
    }
  );
  
  const result = await response.json();
  return result.data.items;
};

// Usage example
const handleAgentDemo = async () => {
  const agent = createAgent("YOUR_API_TOKEN");
  
  try {
    const shortLink = await shorten(agent, 'https://example.com/article');
    console.log(`Created: ${shortLink.shortUrl}`);
    
    const stats = await getStats(agent, shortLink.id);
    console.log("Link stats:", stats);
    
    const searchResults = await searchLinks(agent, "article");
    console.log("Search results:", searchResults);
  } catch (error) {
    console.error("Error:", error);
  }
};
</script>

<template>
  <div>
    <button @click="handleAgentDemo">Run AI Agent Demo</button>
  </div>
</template>

Resources