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 includehttps://orhttp://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 (requiresblock_data.contentandblock_data.slug)"accordion"— expandable content (requiresblock_data.content)"copy-text"— click-to-copy (requiresblock_data.text)"divider"— visual separator (optionalblock_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_modeis"redirect",root_domain_redirect_urlmust be set and valid - Setting
custom_domainto""removes the domain and resets all domain settings - Changing
custom_domainresetsdomain_verifiedto 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 minuteX-RateLimit-Remaining— requests left in this windowX-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_urlfrom create/update responses. Never manually build URLs fromshort_code. - Analytics are under
response.insights.*, not at the root of the response. - Bio link update uses
isVisible(camelCase) — different from the create fieldis_visible. - Short link update uses camelCase:
originalUrl,shortCode— not snake_case. - Setting
customCodeto an already-used code returns asuggestedCodein the error — use it. - Rate limit 429: wait until
X-RateLimit-Resettimestamp before retrying. - URLs must include protocol:
https://example.com✓ vsexample.com✗