返回 Skill 列表
extension
分类: 营销与增长无需 API Key

linkpop

使用Linkpop API创建短链接、管理个人链接页面以及查看点击分析。涵盖认证、URL缩短、个人链接、个人资料更新和在linkpop.space上的分析。

person作者: 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