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.

  1. 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.
  2. 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.
  3. 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

https://b59.link/api

All endpoints accept and return JSON. Errors follow a standard { detail: string } shape.

CallerRate limit (per minute)
No Authorization header2 per IP, with a small burst
Authorization: Bearer <session_key>300 per session key
Tag image /tagCounted per page visitor, not per site

Over the limit you get 429 with a Retry-After header.

POST/api/auth

Get 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}` },
});
POST/api/trackers

Create 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>
GET/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%2Fpricing

Response

{
  "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));
POST/api/shorten

Shorten 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
POST/api/qr

Generate 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);
GET/api/tools/qr

Free 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=svg

Response

200 image/png or image/svg+xml
Cache-Control: public, max-age=86400

Code 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}`;
GET/api/links/lookup

Check 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.com

Response

// 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 working

Error responses

All errors return a JSON body with a detail field.

StatusMeaning
400Bad request — invalid slug format, site address or code
401Invalid or expired session key, unknown API key, or not signed in
403Not your tracker, private stats, or blocked for over-use
404Link not found
409Conflict — URL or slug already exists
422Validation error — missing required field
429Rate limit — 2/min without a session key, 300/min with one
500Internal server error