LinkShrink API Documentation
Privacy-first URL shortener API. Completely free and open — no API keys, no accounts, no signup. Just make HTTP requests.
https://linkshrink.dev/api/v1
|
OpenAPI spec: /openapi.json
No Authentication Required
Every endpoint is open with no authentication. No API key, no OAuth token, no header required. Rate limits apply per IP address (see Rate Limits).
POST /shorten
Shorten a URL. Returns the short link, a QR code URL, a preview URL, anonymous analytics endpoint, and a deletion token. Store the deletion token if you want to delete the link later.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | required | The URL to shorten. Must be http or https. Private IPs and localhost are blocked. |
customAlias | string | optional | Custom slug for the short URL. 3–20 chars, alphanumeric + hyphens only. Must be unique. Example: my-campaign |
ttl_seconds | integer | optional | Link expiry in seconds. Min: 60 (1 minute). Max: 31536000 (365 days). Default: 2592000 (30 days). |
password | string | optional | Protect the link with a password. 4–128 characters. Visitors must enter the password before being redirected. The password is hashed with bcrypt and never stored in plaintext. |
private_stats | boolean | optional | When true, click analytics are private. A statsToken is returned in the response and must be provided via ?token= query parameter when calling GET /stats/:code. Default: false. |
force_preview | boolean | optional | When true, visitors see a safe browsing preview page and must click "Proceed" to reach the destination. Prevents phishing abuse. Default: false. |
Response Fields
| Field | Description |
|---|---|
code | The short code or custom alias |
shortUrl | The full short URL (e.g. https://linkshrink.dev/abc1234) |
previewUrl | Safe preview page showing destination before redirect — no click counted |
qrCode | URL to the QR code PNG for this link |
analytics | URL to fetch anonymous click stats |
deleteToken | Save this. 48-char hex token required to delete the link. Not stored in plaintext. |
statsToken | Only present when private_stats: true. 48-char hex token required to view click analytics. Not stored in plaintext. |
passwordProtected | true if a password was set, false otherwise |
privateStats | true if analytics are private, false otherwise |
forcePreview | true if visitors see a safe browsing preview page before redirect, false otherwise |
createdAt | ISO timestamp of creation |
expiresAt | ISO timestamp of expiry |
Example Request
curl -X POST "https://linkshrink.dev/api/v1/shorten" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/very/long/path?utm_source=email",
"customAlias": "my-campaign",
"ttl_seconds": 86400
}'Example Response
{
"status": "success",
"data": {
"code": "my-campaign",
"shortUrl": "https://linkshrink.dev/my-campaign",
"previewUrl": "https://linkshrink.dev/preview/my-campaign",
"qrCode": "https://linkshrink.dev/api/v1/qr/my-campaign.png",
"analytics": "https://linkshrink.dev/api/v1/stats/my-campaign",
"deleteToken": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6",
"passwordProtected": false,
"originalUrl": "https://example.com/very/long/path?utm_source=email",
"createdAt": "2026-02-20T12:00:00.000Z",
"expiresAt": "2026-02-21T12:00:00.000Z"
}
}POST /shorten/bulk
Shorten up to 50 URLs in a single request. Returns a 207 Multi-Status response with per-URL results. Rate limit: 5 requests per minute.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
urls | string[] | required | Array of URLs to shorten. Maximum 50. Each must be a valid http/https URL. |
ttl_seconds | integer | optional | TTL applied to all links in the batch. |
Example Request
curl -X POST "https://linkshrink.dev/api/v1/shorten/bulk" \
-H "Content-Type: application/json" \
-d '{
"urls": [
"https://example.com/page-1",
"https://example.com/page-2",
"https://example.com/page-3"
],
"ttl_seconds": 604800
}'Example Response
{
"status": "success",
"data": [
{
"url": "https://example.com/page-1",
"success": true,
"data": { "code": "xKp2m4a", "shortUrl": "https://linkshrink.dev/xKp2m4a", ... }
},
{
"url": "https://example.com/page-2",
"success": true,
"data": { "code": "rNq9jzL", "shortUrl": "https://linkshrink.dev/rNq9jzL", ... }
}
]
}POST /shorten/utm
Build a UTM-tagged URL and shorten it in one step. Appends UTM campaign parameters to the destination URL, then creates a short link.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Destination URL |
utm_source | string | Yes | Campaign source (e.g. "google", "newsletter") |
utm_medium | string | Yes | Campaign medium (e.g. "cpc", "email", "social") |
utm_campaign | string | Yes | Campaign name (e.g. "spring-sale") |
utm_term | string | No | Paid search keywords |
utm_content | string | No | Ad/link differentiation |
customAlias | string | No | Custom short code (3-20 chars) |
ttl_seconds | integer | No | Expiry in seconds (60-31536000) |
force_preview | boolean | No | When true, visitors see a safe browsing preview page and must click "Proceed" to reach the destination. Prevents phishing abuse. |
Example Request
curl -X POST "https://linkshrink.dev/api/v1/shorten/utm" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/landing",
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "spring-sale",
"utm_term": "running+shoes",
"utm_content": "hero-banner"
}'Example Response
{
"status": "success",
"data": {
"code": "abc1234",
"shortUrl": "https://linkshrink.dev/abc1234",
"originalUrl": "https://example.com/landing?utm_source=google&utm_medium=cpc&utm_campaign=spring-sale&utm_term=running%2Bshoes&utm_content=hero-banner",
"taggedUrl": "https://example.com/landing?utm_source=google&utm_medium=cpc&utm_campaign=spring-sale&utm_term=running%2Bshoes&utm_content=hero-banner",
"utm": {
"source": "google",
"medium": "cpc",
"campaign": "spring-sale",
"term": "running+shoes",
"content": "hero-banner"
},
"qrCode": "https://linkshrink.dev/api/v1/qr/abc1234.png",
"deleteToken": "a1b2c3d4..."
}
}GET /expand/:code
Look up the original URL for a short code. Note: this increments the click counter. To preview the destination without counting a click, use /preview/:code instead.
| Param | Type | Description |
|---|---|---|
code | path | The short code or custom alias |
curl "https://linkshrink.dev/api/v1/expand/my-campaign"
# Response:
# {
# "status": "success",
# "data": {
# "code": "my-campaign",
# "originalUrl": "https://example.com/very/long/path?utm_source=email"
# }
# }GET /stats/:code
Get anonymous click analytics for a short link. Privacy: no IP addresses, device types, or user identifiers are stored. Only aggregate counts are recorded.
The response includes a clicksByDay array with daily click counts, a referrers array with top referring domains, and a countries array with country-level geographic breakdown — all sorted by volume descending. Country data is derived from IP at click time using a local GeoIP database; the raw IP is never stored.
If the link was created with private_stats: true, you must provide the statsToken via the token query parameter. Returns 401 if the token is missing or invalid.
| Param | Type | Description |
|---|---|---|
code | path | The short code or alias |
token | query (optional) | Stats token for links with private analytics. Required if the link was created with private_stats: true. |
curl "https://linkshrink.dev/api/v1/stats/my-campaign"
# Response:
# {
# "status": "success",
# "data": {
# "code": "my-campaign",
# "originalUrl": "https://example.com/...",
# "clicks": 142,
# "clicksByDay": [
# { "date": "2026-03-10", "clicks": 38 },
# { "date": "2026-03-09", "clicks": 52 },
# { "date": "2026-03-08", "clicks": 52 }
# ],
# "referrers": [
# { "domain": "twitter.com", "clicks": 67 },
# { "domain": "linkedin.com", "clicks": 45 },
# { "domain": "github.com", "clicks": 30 }
# ],
# "countries": [
# { "country": "US", "clicks": 58 },
# { "country": "DE", "clicks": 34 },
# { "country": "PL", "clicks": 28 },
# { "country": "GB", "clicks": 22 }
# ],
# "createdAt": "2026-02-20T12:00:00.000Z",
# "expiresAt": "2026-02-21T12:00:00.000Z",
# "lastClicked": "2026-02-20T14:32:11.000Z"
# }
# }
# For links with private_stats:
curl "https://linkshrink.dev/api/v1/stats/my-campaign?token=YOUR_STATS_TOKEN"
# 401 if token missing/wrong:
# { "error": "Unauthorized", "message": "This link has private analytics..." }GET /stats/:code/export
Export click analytics as CSV or JSON with optional date range filtering. Perfect for importing into spreadsheets, dashboards, or reporting tools. Respects private_stats tokens.
| Param | Type | Description |
|---|---|---|
code | path | The short code or alias |
format | query (optional) | csv or json. Default: json |
from | query (optional) | Start date filter (inclusive), YYYY-MM-DD |
to | query (optional) | End date filter (inclusive), YYYY-MM-DD |
token | query (optional) | Stats token for links with private analytics |
# Export as JSON (default)
curl "https://linkshrink.dev/api/v1/stats/my-campaign/export"
# Export as CSV (opens in Excel/Sheets)
curl "https://linkshrink.dev/api/v1/stats/my-campaign/export?format=csv" -o analytics.csv
# Filter by date range
curl "https://linkshrink.dev/api/v1/stats/my-campaign/export?from=2026-03-01&to=2026-03-14"
# JSON Response:
# {
# "status": "success",
# "data": {
# "code": "my-campaign",
# "originalUrl": "https://example.com/...",
# "from": "2026-03-01",
# "to": "2026-03-14",
# "totalClicks": 142,
# "clicksByDay": [
# { "date": "2026-03-01", "clicks": 12 },
# { "date": "2026-03-02", "clicks": 28 },
# ...
# ]
# }
# }
# CSV Response:
# date,clicks
# 2026-03-01,12
# 2026-03-02,28
# ...POST /stats/batch
Get click statistics for multiple short codes in a single request. Returns 207 Multi-Status with per-code results. Maximum 50 codes per request. Supports tokens object for links with private analytics.
| Field | Type | Description |
|---|---|---|
codes | string[] (required) | Array of short codes to query (max 50) |
tokens | object (optional) | Map of { "code": "statsToken" } for private-stats links |
curl -X POST "https://linkshrink.dev/api/v1/stats/batch" \
-H "Content-Type: application/json" \
-d '{
"codes": ["my-campaign", "launch-day", "newsletter-42"],
"tokens": { "my-campaign": "optional-stats-token" }
}'
# Response (207 Multi-Status):
# {
# "status": "success",
# "data": [
# {
# "code": "my-campaign",
# "success": true,
# "data": { "clicks": 142, "clicksByDay": [...], ... }
# },
# {
# "code": "launch-day",
# "success": true,
# "data": { "clicks": 89, "clicksByDay": [...], ... }
# },
# {
# "code": "newsletter-42",
# "success": false,
# "error": "Short URL not found or expired"
# }
# ]
# }GET /qr/:code.png & /qr/:code.svg
Returns a QR code for the short URL in PNG or SVG format. Supports custom foreground and background colors for branded QR codes. Response is cached for 24 hours.
| Param | Type | Description |
|---|---|---|
code | path | The short code or alias |
size | query (optional) | QR image size in pixels. Min: 64, Max: 1024, Default: 512 |
fg | query (optional) | Foreground (dark modules) hex color, e.g. 000000 or #ff00aa. Default: #0a0a0f |
bg | query (optional) | Background hex color, e.g. ffffff or #00f0ff. Default: #00f0ff |
# Default PNG (512×512px, cyberpunk colors) curl "https://linkshrink.dev/api/v1/qr/my-campaign.png" --output qr.png # Custom size (256px) curl "https://linkshrink.dev/api/v1/qr/my-campaign.png?size=256" --output qr-small.png # SVG format (scalable, print-ready) curl "https://linkshrink.dev/api/v1/qr/my-campaign.svg" --output qr.svg # Custom colors — black on white curl "https://linkshrink.dev/api/v1/qr/my-campaign.png?fg=000000&bg=ffffff" --output qr-bw.png # Branded colors — SVG with custom hex curl "https://linkshrink.dev/api/v1/qr/my-campaign.svg?fg=ff00aa&bg=0a0a0f" --output qr-brand.svg # Use directly in an <img> tag (PNG or SVG): # <img src="https://linkshrink.dev/api/v1/qr/my-campaign.svg" alt="QR Code">
DELETE /:code
Permanently delete a short link. Requires the delete_token returned when the link was created. After deletion the link returns 404.
deleteToken from the creation response — it is the only way to delete the link. The token is stored as a hash and cannot be recovered if lost.
| Param | Type | Description |
|---|---|---|
code | path | The short code or alias to delete |
delete_token | query (required) | The deletion token from the creation response |
curl -X DELETE \
"https://linkshrink.dev/api/v1/my-campaign?delete_token=a1b2c3d4e5f6..."
# Success response:
# {
# "status": "success",
# "message": "Short URL deleted successfully",
# "data": { "code": "my-campaign" }
# }
# Error (missing/wrong token):
# HTTP 401
# {
# "error": "Unauthorized",
# "message": "A delete_token is required to delete this link",
# "hint": "Use the delete_token returned when the link was created"
# }POST /verify/:code
Verify the password for a password-protected short URL. Returns the original URL on success. For non-password-protected links, returns the URL directly without requiring a password.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
password | string | required | The password set when the link was created |
Example
curl -X POST "https://linkshrink.dev/api/v1/verify/my-secret-link" \
-H "Content-Type: application/json" \
-d '{"password": "s3cret"}'
# Success — 200:
# { "status": "success", "data": { "code": "my-secret-link", "originalUrl": "https://example.com/secret" } }
# Wrong password — 401:
# { "error": "Invalid password", "message": "The provided password is incorrect." }GET /preview/:code
Shows the destination URL in a safety page before redirecting. Does not increment the click counter. Useful for sharing links where recipients want to verify the destination before visiting. Password-protected links show the password form instead.
Example: https://linkshrink.dev/preview/my-campaign shows a page with the destination URL and a "Proceed to site" button.
GET /unshorten
Resolve any shortened URL to its final destination by following redirect chains. Works with any URL shortener — Bitly, TinyURL, t.co, goo.gl, and custom domains. Follows up to 10 redirect hops and returns the full redirect chain.
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string (query) | Yes | The shortened URL to resolve (URL-encoded) |
curl "https://linkshrink.dev/api/v1/unshorten?url=https%3A%2F%2Fbit.ly%2Fexample"
{
"status": "success",
"data": {
"originalUrl": "https://bit.ly/example",
"destinationUrl": "https://example.com/final-page",
"redirectChain": [
{ "url": "https://bit.ly/example", "statusCode": 301 }
],
"redirectCount": 1
}
}Response Fields
| Field | Type | Description |
|---|---|---|
originalUrl | string | The input URL you provided |
destinationUrl | string | The final destination URL after following all redirects |
redirectChain | array | Each redirect hop with url and statusCode |
redirectCount | number | Number of redirects followed |
Response Format
All API responses are JSON. Successful responses include "status": "success". Error responses include "error" and "message" fields.
{
"status": "success",
"data": { ... }
}{
"error": "Failed to shorten URL",
"message": "This alias is already taken"
}Error Codes
| Status | Meaning |
|---|---|
400 | Bad Request — Missing or invalid input (URL, alias format, TTL out of range) |
401 | Unauthorized — Missing or invalid delete_token or statsToken |
404 | Not Found — Short code doesn't exist or has expired |
429 | Too Many Requests — Rate limit exceeded (see below) |
500 | Internal Server Error — Something went wrong on our side |
Rate Limits
Rate limits are per IP address per minute.
| Endpoint | Limit | Window |
|---|---|---|
| POST /shorten | 20 requests | 1 minute |
| POST /shorten/bulk | 5 requests | 1 minute |
| GET /stats/:code, GET /stats/:code/export, POST /stats/batch | 60 requests | 1 minute |
| GET /expand/:code, /qr/:code.png, /qr/:code.svg, DELETE /:code | 100 requests | 1 minute |
| GET /unshorten | 30 requests | 1 minute |
When rate limited the API returns 429 with a standard Retry-After HTTP header indicating the number of seconds to wait before retrying. Wait for the specified duration and retry.
cURL Examples
# Shorten (with custom alias and 7-day TTL)
curl -X POST "https://linkshrink.dev/api/v1/shorten" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com","customAlias":"my-link","ttl_seconds":604800}'
# Get click stats
curl "https://linkshrink.dev/api/v1/stats/my-link"
# Download QR code (PNG, 512px)
curl "https://linkshrink.dev/api/v1/qr/my-link.png" --output qr.png
# Download SVG QR with custom brand colors
curl "https://linkshrink.dev/api/v1/qr/my-link.svg?fg=ff00aa&bg=ffffff" --output qr.svg
# Delete (requires deleteToken from creation response)
curl -X DELETE "https://linkshrink.dev/api/v1/my-link?delete_token=<token>"
# Bulk shorten 3 URLs
curl -X POST "https://linkshrink.dev/api/v1/shorten/bulk" \
-H "Content-Type: application/json" \
-d '{"urls":["https://example.com/1","https://example.com/2","https://example.com/3"]}'JavaScript
// Shorten a URL
const res = await fetch('https://linkshrink.dev/api/v1/shorten', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: 'https://example.com/very/long/path',
customAlias: 'my-link', // optional
ttl_seconds: 86400 // optional — 1 day
})
});
const { data } = await res.json();
console.log(data.shortUrl); // https://linkshrink.dev/my-link
console.log(data.previewUrl); // https://linkshrink.dev/preview/my-link
console.log(data.qrCode); // QR code PNG URL
console.log(data.deleteToken); // Save this!
// Get click count
const stats = await fetch(`https://linkshrink.dev/api/v1/stats/${data.code}`);
const { data: s } = await stats.json();
console.log(s.clicks); // 42
// Delete the link
await fetch(
`https://linkshrink.dev/api/v1/${data.code}?delete_token=${data.deleteToken}`,
{ method: 'DELETE' }
);Python
import requests
BASE = "https://linkshrink.dev/api/v1"
# Shorten a URL
response = requests.post(f"{BASE}/shorten", json={
"url": "https://example.com/very/long/path",
"customAlias": "my-link", # optional
"ttl_seconds": 86400 # optional — 1 day
})
data = response.json()["data"]
print(data["shortUrl"]) # https://linkshrink.dev/my-link
print(data["deleteToken"]) # save this
# Get click stats
stats = requests.get(f"{BASE}/stats/{data['code']}").json()["data"]
print(stats["clicks"]) # 42
# Download QR code (PNG)
qr = requests.get(f"{BASE}/qr/{data['code']}.png?size=256")
with open("qr.png", "wb") as f:
f.write(qr.content)
# Download branded SVG QR code
svg = requests.get(f"{BASE}/qr/{data['code']}.svg?fg=ff00aa&bg=ffffff")
with open("qr.svg", "w") as f:
f.write(svg.text)
# Delete the link
requests.delete(
f"{BASE}/{data['code']}",
params={"delete_token": data["deleteToken"]}
)
# Bulk shorten
bulk = requests.post(f"{BASE}/shorten/bulk", json={
"urls": ["https://example.com/1", "https://example.com/2"],
"ttl_seconds": 604800
}).json()
for item in bulk["data"]:
print(item["data"]["shortUrl"] if item["success"] else item["error"])MCP Integration
LinkShrink supports the Model Context Protocol (MCP), letting AI assistants like Claude, Cursor, and VS Code Copilot use LinkShrink tools directly from the chat window.
Installation
{
"mcpServers": {
"linkshrink": {
"command": "npx",
"args": ["-y", "linkshrink-mcp"]
}
}
}Available Tools
| Tool | Description | Parameters |
|---|---|---|
shorten_url |
Shorten a long URL | url (required), customAlias (optional) |
expand_url |
Expand a short code to its original URL | code (required) |
url_stats |
Get anonymous click statistics | code (required) |