Back to skills
extension
Category: Marketing & GrowthNo API key required

linkpop

Use the Linkpop API to create short links, manage bio link pages, and view click analytics. Covers auth, URL shortening, bio links, profile updates, and analytics at linkpop.space.

personAuthor: jakexiaohubgithub

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