b59.link API
Authentication
Every endpoint works without signing in, at 2 requests per minute. Sign in once to get a session key and send it as Authorization: Bearer <session_key> for 300 requests per minute. Things you create while signed in belong to your account. A bot can do all of this on its own, without opening the website.
- Have an API key?
POST /api/auth { api_key }returns a session key straight away. Every account has a 256-character key on its profile page. - No key?
POST /api/auth { email }emails a 6-digit code. Call it again with{ email, otp }to get a session key and the account's API key. New emails are signed up automatically. - Send the session key on every call. It lasts 30 days; an invalid or expired key gets
401.
The API and MCP server create links, QR codes and trackers and return their stats URLs. The stats themselves are shown on b59.link, not returned by the API.
Overview
A simple REST API for page-view trackers, short links and QR codes. Create a tracker and get the tag image URL for your app or site pages, shorten links, make QR codes, and look up the stats page for anything b59.link tracks.
Base URL
All endpoints accept and return JSON. Errors follow a standard { detail: string } shape.
| Caller | Rate limit (per minute) |
|---|---|
| No Authorization header | 2 per IP, with a small burst |
| Authorization: Bearer <session_key> | 300 per session key |
Tag image /tag | Counted per page visitor, not per site |
Over the limit you get 429 with a Retry-After header.
/api/authGet a session key. Send an api_key for an instant session, or an email to receive a 6-digit code and then the email plus otp. Also: GET /api/auth/me (who am I), POST /api/auth/logout (revoke the session key).
Request body
{ "api_key": "<256-char key>" }
// or, step 1
{ "email": "bot@example.com" }
// step 2
{ "email": "bot@example.com", "otp": "123456" }Response
{
"session_key": "b59s_...",
"token_type": "Bearer",
"expires_at": "2026-11-02T10:00:00Z",
"email": "bot@example.com",
"api_key": "<256 chars>" // after email + otp
}
// step 1 returns
{ "otp_sent": true, "email": "...", "expires_in_seconds": 600 }Code sample
// 1a. Have an API key? Swap it for a session key
let res = await fetch("https://b59.link/api/auth", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ api_key: process.env.B59_API_KEY }),
});
const { session_key } = await res.json();
// 1b. No key? Email code instead (signs the email up if new)
await fetch("https://b59.link/api/auth", { method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "bot@example.com" }) });
// ...read the 6-digit code from the inbox, then:
res = await fetch("https://b59.link/api/auth", { method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "bot@example.com", otp: "123456" }) });
// -> { session_key, api_key, expires_at, email }
// 2. Send it on every call for 300 requests/minute
await fetch("https://b59.link/api/auth/me", {
headers: { Authorization: `Bearer ${session_key}` },
});/api/trackersCreate a page-view tracker. Returns the tag image URL and ready-made snippets. Views are counted only on site_url's domain and its subdomains; bots and Do-Not-Track visitors are skipped. With a session key the tracker is yours; without one you get a manage_token to claim it into an account later.
Request body
{
"site_url": "example.com",
"name": "My blog" // optional
}Response
{
"uuid": "6f1c...-...",
"domain": "example.com",
"tag_url": "https://b59.link/tag?6f1c...",
"snippet_img": "<img src=... referrerpolicy=...>",
"snippet_js": "<script>...</script><noscript>...</noscript>",
"stats_url": "https://b59.link/tracker/6f1c...",
"manage_token": "..." // only when not signed in
}Code sample
// Create a page-view tracker and get the tag for your pages
const res = await fetch("https://b59.link/api/trackers", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${sessionKey}`, // optional: the tracker belongs to you
},
body: JSON.stringify({ site_url: "example.com", name: "My blog" }),
});
const t = await res.json();
console.log(t.tag_url); // https://b59.link/tag?<uuid>
console.log(t.snippet_js); // paste before </body>
console.log(t.stats_url); // https://b59.link/tracker/<uuid>/api/stats-url?q=Find the stats page for anything b59.link tracks: a slug or short URL, the destination URL of a short link or QR code, a tracker id or tag URL, or any page URL or domain with a tracker tag. Returns URLs only; stats are viewed on b59.link.
Request body
GET /api/stats-url?q=https%3A%2F%2Fexample.com%2FpricingResponse
{
"found": true,
"results": [
{ "type": "page_tracker", "tracker_id": "6f1c...",
"tag_url": "https://b59.link/tag?6f1c...",
"stats_url": "https://b59.link/tracker/6f1c..." },
{ "type": "short_link", "slug": "cool-jump",
"short_url": "https://b59.link/cool-jump",
"stats_url": "https://b59.link/stats?id=cool-jump" }
]
}Code sample
// Where are the stats for this page / link / QR code?
const q = "https://example.com/pricing"; // or a slug, short URL, domain, tracker id
const res = await fetch(`https://b59.link/api/stats-url?q=${encodeURIComponent(q)}`);
const { found, results } = await res.json();
results.forEach((r) => console.log(r.type, r.stats_url));/api/shortenShorten a long URL. Omit slug to get suggestions; include slug to create the link immediately.
Request body
{
"url": "https://example.com/very/long/url",
"slug": "my-link" // optional
}Response
// Without slug — returns suggestions:
{ "suggestions": ["cool-jump", "fast-fly", "slim-run"] }
// With slug — creates the link:
{
"slug": "my-link",
"short_url": "https://b59.link/my-link",
"stats_url": "https://b59.link/stats?id=my-link",
"qr_code_data": "<base64 PNG>"
}Code sample
// Shorten a URL
const res = await fetch("https://b59.link/api/shorten", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: "https://example.com/very/long/path",
slug: "my-link", // optional
}),
});
const data = await res.json();
console.log(data.short_url); // https://b59.link/my-link
console.log(data.slug); // my-link/api/qrGenerate a trackable QR code for a URL. Also creates a short link in one call so scans are counted. Returns a base64-encoded PNG and the stats URL.
Request body
{
"url": "https://example.com"
}Response
{
"slug": "cool-jump",
"short_url": "https://b59.link/cool-jump",
"stats_url": "https://b59.link/stats?id=cool-jump",
"qr_code_data": "<base64 PNG>"
}Code sample
// Generate a QR code (also creates a short link)
const res = await fetch("https://b59.link/api/qr", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: "https://example.com" }),
});
const data = await res.json();
// data.qr_code_data is a base64-encoded PNG
const img = document.createElement("img");
img.src = `data:image/png;base64,${data.qr_code_data}`;
document.body.appendChild(img);/api/tools/qrFree QR code image for any text: URL, Wi-Fi, mailto:, tel:, plain text. Returns the image itself, so the URL can go straight into an <img>. No short link is created. Params: data (required, max 1500 chars), size 64–1200 (300), fg / bg hex colors, format png|svg, ecc L|M|Q|H, margin 0–10, download=true.
Request body
GET /api/tools/qr?data=https%3A%2F%2Fexample.com&size=400&fg=09c269&format=svgResponse
200 image/png or image/svg+xml
Cache-Control: public, max-age=86400Code sample
// Any text -> QR image. Use the URL directly in an <img>.
const params = new URLSearchParams({
data: "WIFI:T:WPA;S:Cafe;P:coffee123;;",
size: "400", fg: "09c269", bg: "ffffff", format: "svg",
});
document.querySelector("img").src = `https://b59.link/api/tools/qr?${params}`;/api/links/lookupCheck if a URL already has a b59.link. Automatically tries http/https, www, and trailing-slash variations.
Request body
GET /api/links/lookup?url=https%3A%2F%2Fexample.comResponse
// Found:
{
"found": true,
"slug": "cool-jump",
"short_url": "https://b59.link/cool-jump",
"stats_url": "https://b59.link/stats?id=cool-jump",
"qr_code_data": "<base64 PNG or null>"
}
// Not found:
{ "found": false }Code sample
// Check if a URL already has a b59 link (handles http/https/www variations)
const url = "https://example.com";
const res = await fetch(
`https://b59.link/api/links/lookup?url=${encodeURIComponent(url)}`
);
const data = await res.json();
if (data.found) {
console.log(data.short_url); // https://b59.link/cool-jump
console.log(data.stats_url); // https://b59.link/stats/cool-jump
// data.qr_code_data — base64 PNG if QR exists
} else {
console.log("No link yet — create one with POST /qr");
}Manage trackers
These need a session key (or a website login). You can only change your own trackers.
GET /api/trackers/mine # your trackers with tag + stats URLs
PATCH /api/trackers/{uuid} # { "name"?, "site_url"?, "is_public"? }
DELETE /api/trackers/{uuid}
POST /api/trackers/{uuid}/claim # { "manage_token" } — adopt one made while signed out
GET /api/auth/api-key # your API key
POST /api/auth/api-key/rotate # new key; the old one stops workingError responses
All errors return a JSON body with a detail field.
| Status | Meaning |
|---|---|
| 400 | Bad request — invalid slug format, site address or code |
| 401 | Invalid or expired session key, unknown API key, or not signed in |
| 403 | Not your tracker, private stats, or blocked for over-use |
| 404 | Link not found |
| 409 | Conflict — URL or slug already exists |
| 422 | Validation error — missing required field |
| 429 | Rate limit — 2/min without a session key, 300/min with one |
| 500 | Internal server error |