linkpop

SKILL.md

Linkpop Skill

Use this skill when the user wants to shorten URLs, manage a bio link page, or view click analytics using Linkpop (linkpop.space).


Base URL

https://linkpop.space

Authentication

All protected endpoints require a Bearer token in the Authorization header:

Authorization: Bearer YOUR_TOKEN

You get the token from the signup or login response. Store it immediately — you'll need it for every subsequent request.


IMPORTANT: Response Field Guide

Several endpoints return duplicate fields with different names for compatibility. Here is what to use:

Endpoint Field confusion What to use
Signup / Login token and api_token are identical Use token
Create/Update bio link Response has both link and bioLink (identical objects) Use either
List bio links Response has both links and bioLinks (identical arrays) Use either
Create short link Use short_url from the response, not url.short_code short_url is the full ready-to-share URL
Analytics overview Data is nested under insights key Access response.insights.totalClicks etc.

Endpoint Reference

1. Create Account

POST /api/auth/signup

Request body:

{
  "email": "user@example.com",
  "password": "atleast8chars",
  "username": "myusername"
}

Username rules: 3–30 chars, letters/numbers/hyphens/underscores only. Cannot be reserved words like admin, api, dashboard, s, analytics, linktree, bitly.

Response (HTTP 201):

{
  "success": true,
  "user": {
    "id": "uuid",
    "email": "user@example.com",
    "username": "myusername",
    "display_name": null,
    "bio": null,
    "avatar_url": null,
    "theme": "light",
    "created_at": "...",
    "updated_at": "..."
  },
  "token": "your-session-token",
  "api_token": "your-session-token",
  "profile_url": "https://myusername.linkpop.space"
}

token and api_token are identical — use token. profile_url is the user's public bio page.


2. Login

POST /api/auth/login

Request body:

{
  "email": "user@example.com",
  "password": "yourpassword"
}

Response (HTTP 200):

{
  "success": true,
  "user": { "id": "...", "username": "myusername", "email": "..." },
  "token": "your-session-token",
  "api_token": "your-session-token",
  "profile_url": "https://myusername.linkpop.space"
}

3. Get Current User

GET /api/auth/me
Requires auth.

Response:

{
  "user": {
    "id": "uuid",
    "email": "...",
    "username": "myusername",
    "display_name": "My Name",
    "bio": "My bio",
    "avatar_url": null,
    "custom_domain": null,
    "root_domain_mode": "bio",
    "root_domain_redirect_url": null,
    "use_domain_for_shortlinks": true
  }
}

4. Create Short Link

POST /api/urls
Requires auth.

Request body:

{
  "originalUrl": "https://example.com/very/long/url",
  "customCode": "my-link",
  "title": "My Link Title"
}
  • originalUrl — required, must include https:// or http://
  • customCode — optional, 3–100 chars, letters/numbers/hyphens/underscores. Short codes are unique per user (not globally), so two different users can have the same code.
  • title — optional, max 255 chars

Response (HTTP 201):

{
  "success": true,
  "url": {
    "id": "uuid",
    "short_code": "my-link",
    "original_url": "https://example.com/very/long/url",
    "title": "My Link Title",
    "clicks": 0,
    "is_active": true,
    "custom_code": true,
    "user_id": "uuid",
    "created_at": "...",
    "updated_at": "..."
  },
  "short_url": "https://myusername.linkpop.space/my-link"
}

Always use short_url — it is the complete, ready-to-share URL. It respects custom domain settings automatically. Do not construct URLs manually from url.short_code.

If customCode is already taken by this user, the API returns a suggestedCode alternative in the error response:

{ "error": "You already have a link with this code. Try: my-link123", "suggestedCode": "my-link123" }

5. List Short Links

GET /api/urls
Requires auth.

Response:

{
  "urls": [
    {
      "id": "uuid",
      "short_code": "my-link",
      "original_url": "https://example.com",
      "title": "My Link Title",
      "clicks": 42,
      "is_active": true,
      "created_at": "..."
    }
  ]
}

6. Update Short Link

PATCH /api/urls/{link_id}
Requires auth.

Request body (all fields optional):

{
  "originalUrl": "https://new-url.com",
  "title": "New Title",
  "shortCode": "new-code"
}

Note: all fields are camelCase (originalUrl, shortCode), not snake_case.

Response:

{
  "success": true,
  "url": { "id": "...", "short_code": "new-code", "original_url": "...", "title": "...", "clicks": 42 },
  "short_url": "https://myusername.linkpop.space/new-code"
}

7. Delete Short Link

DELETE /api/urls/{link_id}
Requires auth.

Response:

{ "success": true, "message": "URL deleted successfully", "deleted": true }

8. Create Bio Link

POST /api/bio-links
Requires auth.

Request body:

{
  "title": "My Instagram",
  "url": "https://instagram.com/myprofile",
  "block_type": "link",
  "is_visible": true
}

block_type options:

  • "link" — standard clickable link (default, recommended)
  • "social" — social media link with auto-detected platform icon (detected from URL hostname)
  • "page" — full markdown page (requires block_data.content and block_data.slug)
  • "accordion" — expandable content (requires block_data.content)
  • "copy-text" — click-to-copy (requires block_data.text)
  • "divider" — visual separator (optional block_data.showTitle)

Response (HTTP 201):

{
  "success": true,
  "link": {
    "id": "uuid",
    "title": "My Instagram",
    "url": "https://instagram.com/myprofile",
    "block_type": "link",
    "is_visible": true,
    "position": 0,
    "user_id": "uuid",
    "created_at": "..."
  },
  "bioLink": { "...same object as link..." }
}

link and bioLink are always identical — use either one.


9. List Bio Links

GET /api/bio-links
Requires auth.

Response:

{
  "success": true,
  "links": [ { "id": "...", "title": "...", "url": "...", "block_type": "link", "is_visible": true, "position": 0 } ],
  "bioLinks": [ "...same array as links..." ],
  "count": 1
}

links and bioLinks are always identical — use either one. count is the total number of bio links.


10. Update Bio Link

PATCH /api/bio-links/{link_id}
Requires auth.

Request body (all optional):

{
  "title": "Updated Title",
  "url": "https://newurl.com",
  "icon": null,
  "isVisible": false,
  "block_data": {}
}

Important: visibility field here is isVisible (camelCase), NOT is_visible. This is different from the create endpoint.

Response:

{
  "success": true,
  "link": { "id": "...", "title": "Updated Title", "url": "...", "is_visible": false },
  "bioLink": { "...same as link..." }
}

11. Delete Bio Link

DELETE /api/bio-links/{link_id}
Requires auth.

Response:

{ "success": true, "message": "Bio link deleted successfully", "deleted": true }

12. Reorder Bio Links

POST /api/bio-links/reorder
Requires auth.

The array order determines display order on the bio page (index 0 = top).

Request body:

{
  "linkIds": ["uuid1", "uuid2", "uuid3"]
}

Response:

{ "success": true, "message": "Bio links reordered successfully", "reordered": true, "count": 3 }

13. Update Profile

PATCH /api/profile
Requires auth.

All fields optional:

{
  "display_name": "My Display Name",
  "bio": "Bio text up to 500 chars",
  "avatar_url": "https://example.com/avatar.png",
  "profile_image_url": "https://example.com/image.png",
  "theme": "dark",
  "background_type": "gradient",
  "background_value": "#ff0000",
  "font_family": "Inter",
  "custom_domain": "mysite.com",
  "use_domain_for_shortlinks": true,
  "root_domain_mode": "bio",
  "root_domain_redirect_url": "https://mysite.com/landing"
}
  • theme: "default" | "dark" | "light"
  • background_type: "solid" | "gradient" | "image"
  • root_domain_mode: "bio" (show profile at root) | "redirect" (redirect root to another URL)
  • If root_domain_mode is "redirect", root_domain_redirect_url must be set and valid
  • Setting custom_domain to "" removes the domain and resets all domain settings
  • Changing custom_domain resets domain_verified to false — you must re-verify

Response:

{ "success": true, "message": "Profile updated successfully", "updated": true }

14. Get Analytics Overview

GET /api/insights
Requires auth. Optional query params: ?startDate=2025-01-01&endDate=2025-01-31

Response — note data is nested under the insights key:

{
  "insights": {
    "totalClicks": 1234,
    "urlClicks": 800,
    "bioLinkClicks": 434,
    "clicksToday": 50,
    "clicksThisWeek": 300,
    "clicksThisMonth": 1000,
    "topUrls": [
      { "id": "uuid", "title": "My Link", "short_code": "my-link", "url": "https://example.com", "clicks": 100 }
    ],
    "topBioLinks": [
      { "id": "uuid", "title": "Instagram", "url": "https://instagram.com/...", "clicks": 50 }
    ],
    "recentClicks": [
      { "id": "uuid", "type": "url", "title": "My Link", "clicked_at": "...", "country": "US", "city": "New York" }
    ],
    "clicksByDay": [
      { "date": "2025-01-29", "clicks": 50, "urlClicks": 30, "bioLinkClicks": 20 }
    ]
  }
}

Access data as response.insights.totalClicks, not response.totalClicks.


15. Get Short Link Analytics

GET /api/insights/shortlinks/{shortlink_id}
Requires auth. Optional query params: ?startDate=...&endDate=...

The {shortlink_id} is the UUID from url.id, not the short code string.

Response:

{
  "link": {
    "id": "uuid",
    "short_code": "my-link",
    "destination_url": "https://example.com",
    "title": "My Link",
    "total_clicks": 500,
    "created_at": "..."
  },
  "clicksByDay": [ { "date": "2025-01-29", "clicks": 50 } ],
  "topCountries": [ { "country": "United States", "clicks": 200 } ],
  "topCities": [ { "city": "New York", "country": "United States", "clicks": 50 } ],
  "topBrowsers": [ { "browser": "Chrome", "version": "120.0", "clicks": 300 } ],
  "topOS": [ { "os": "Windows", "version": "11", "clicks": 250 } ],
  "topReferrers": [ { "platform": "twitter", "referrer": "https://t.co/...", "clicks": 100 } ],
  "deviceTypes": [ { "deviceType": "mobile", "clicks": 300 }, { "deviceType": "desktop", "clicks": 200 } ],
  "summary": {
    "totalClicks": 500,
    "dateRange": { "start": "...", "end": "..." }
  }
}

16. Get Bio Page Analytics

GET /api/insights/pages
Requires auth. Optional query params: ?startDate=...&endDate=...

Returns profile view stats and per-link CTR:

{
  "overview": { "profileViews": 1000, "linkClicks": 200, "ctr": 20.0 },
  "viewsByDay": [ { "date": "2025-01-29", "views": 50 } ],
  "topLinks": [ { "id": "uuid", "title": "Instagram", "url": "...", "clicks": 80, "ctr": 8.0 } ],
  "topCountries": [ { "country": "IN", "views": 400 } ],
  "deviceTypes": [ { "deviceType": "mobile", "views": 600 } ]
}

17. Get Subscription Info

GET /api/subscription
Requires auth.

Response:

{
  "tier": "free",
  "expiresAt": null,
  "limits": {
    "maxLinks": -1,
    "maxUrls": -1,
    "analyticsRetentionDays": 365,
    "customDomain": true,
    "customJS": true,
    "advancedBlocks": true,
    "removeWatermark": true
  }
}

-1 means unlimited. Currently all users (free and pro) get unlimited everything with 365-day analytics retention.


Error Responses

All errors return JSON with an error field:

{ "error": "Human-readable error message" }
HTTP Status Meaning
400 Bad request — invalid input or validation failure
401 Unauthorized — missing or invalid token
403 Forbidden — feature requires upgrade
404 Not found
429 Rate limited — check X-RateLimit-Reset header
500 Server error
503 Database temporarily unavailable — retry

Rate limit headers on every response:

  • X-RateLimit-Limit — max requests per minute
  • X-RateLimit-Remaining — requests left in this window
  • X-RateLimit-Reset — ISO timestamp when the window resets

Rate limits: signup = 20/min, most endpoints = 100/min per user.


Complete Workflow Example

// 1. Sign up
const signup = await fetch('https://linkpop.space/api/auth/signup', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: 'me@example.com', password: 'password123', username: 'mybot' })
})
const { token, profile_url } = await signup.json()
// profile_url = "https://mybot.linkpop.space"

const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }

// 2. Create a short link
const urlRes = await fetch('https://linkpop.space/api/urls', {
  method: 'POST',
  headers,
  body: JSON.stringify({ originalUrl: 'https://example.com/article', customCode: 'article', title: 'My Article' })
})
const { short_url } = await urlRes.json()
// short_url = "https://mybot.linkpop.space/article" — use this directly

// 3. Add a bio link
await fetch('https://linkpop.space/api/bio-links', {
  method: 'POST',
  headers,
  body: JSON.stringify({ title: 'My Article', url: short_url, block_type: 'link' })
})

// 4. Get analytics — remember to access .insights
const analyticsRes = await fetch('https://linkpop.space/api/insights', { headers })
const { insights } = await analyticsRes.json()
console.log(insights.totalClicks, insights.clicksToday)

// 5. Hide a bio link (note: isVisible camelCase, not is_visible)
await fetch(`https://linkpop.space/api/bio-links/${linkId}`, {
  method: 'PATCH',
  headers,
  body: JSON.stringify({ isVisible: false })
})

Gotchas & Common Mistakes

  • Short codes are per-user, not global. Two different users can both have a short code "blog". When routing, the subdomain (username.linkpop.space) determines which user's link is resolved.
  • Always use short_url from create/update responses. Never manually build URLs from short_code.
  • Analytics are under response.insights.*, not at the root of the response.
  • Bio link update uses isVisible (camelCase) — different from the create field is_visible.
  • Short link update uses camelCase: originalUrl, shortCode — not snake_case.
  • Setting customCode to an already-used code returns a suggestedCode in the error — use it.
  • Rate limit 429: wait until X-RateLimit-Reset timestamp before retrying.
  • URLs must include protocol: https://example.com ✓ vs example.com
Weekly Installs
3
First Seen
Mar 2, 2026
Installed on
gemini-cli3
github-copilot3
codex3
kimi-cli3
cursor3
amp3