The official JavaScript SDK for INBIO, the premium URL shortener with click analytics and customizable QR codes. Shorten links, generate styled QR codes, read analytics, and manage folders and tags — from Node.js, browsers, or edge runtimes.
- Free to start —
shorten()and the QR API need no account and no API key - Zero runtime dependencies — built on the global
fetch(Node ≥ 18); ESM + CJS with full TypeScript types - Complete — covers the entire INBIO REST API: links CRUD, auto-pagination, bulk create, QR codes, analytics, webhook signature verification
npm install inbio-sdkimport { shorten } from "inbio-sdk";
const { short_url } = await shorten("https://example.com/some/very/long/path");
console.log(short_url); // https://in.bio/x1Y2z3Anonymous links are rate limited (5/min per IP, 100/day) and deleted after 30 days
unless claimed via the returned claim_url. The result also includes slug,
qr_url, preview_url and expires.
Create a token under Settings → API tokens (API access is a Pro/Business feature), then:
import Inbio from "inbio-sdk";
const client = new Inbio({ token: "your-token" }); // or set INBIO_API_TOKEN
const link = await client.links.create("https://example.com/sale", {
slug: "spring-sale",
tags: ["marketing"],
});
console.log(link.short_url); // https://in.bio/spring-saleclient.shorten(url) is also available and stays keyless.
links.list() returns one page; links.iterate() is an async iterator that
auto-paginates through every link:
const page = await client.links.list({ status: "active", perPage: 50 });
console.log(page.total, page.currentPage, page.hasNextPage);
for await (const link of client.links.iterate({ tag: "marketing" })) {
console.log(link.slug, link.total_clicks);
}Filters: search, folderId, tag, status, page, perPage.
const link = await client.links.create("https://example.com/launch", {
slug: "launch", // 4-64 chars [a-zA-Z0-9-_]; omitted -> random
title: "Product launch",
redirectType: 301, // 301, 302 (default) or 307
folderId: 3,
tags: ["launch", "2026"],
expiresAt: new Date("2026-12-31T23:59:59Z"), // Pro+
fallbackUrl: "https://example.com", // shown after expiry; Pro+
clickLimit: 10_000, // Pro+
password: "s3cret", // Pro+
utm: { source: "newsletter", campaign: "launch" },
});Option names are camelCase and map 1:1 to the API's snake_case fields; response
objects keep the API's exact field names (short_url, total_clicks, ...).
const link = await client.links.get(42);
await client.links.update(42, { title: "New title", destinationUrl: "https://example.com/v2" });
await client.links.disable(42); // active -> disabled
await client.links.enable(42); // disabled -> active (anything else throws StateConflictError)
await client.links.delete(42); // 204; the link stops redirectingUp to 100 links per call; rows fail independently:
const { created, failed } = await client.links.bulkCreate([
{ destinationUrl: "https://example.com/a", slug: "a1" },
{ destinationUrl: "https://example.com/b", slug: "b1" },
]);
failed.forEach((f) => console.warn(`row ${f.index}: ${f.error}`));links.qr() returns the raw image bytes (png default, or svg, 64-2048 px).
The QR encodes the short URL, so destination edits never invalidate printed codes:
import { writeFileSync } from "node:fs";
writeFileSync("spring-sale.png", await client.links.qr(42, { format: "png", size: 1024 }));const stats = await client.links.analytics(42, { from: "2026-06-01", to: "2026-07-01" });
console.log(stats.totals); // { clicks, uniques, botClicks }
console.log(stats.series[0]); // { date, clicks, uniques }
console.log(stats.countries); // top 10: [{ value: "US", clicks: 512 }, ...]
// also: stats.devices, stats.browsers, stats.referrers, stats.rangefrom/to accept YYYY-MM-DD strings or Date objects; the range is clamped to
your plan's analytics retention.
const folders = await client.folders.list(); // [{ id, name, color, position, links_count, created_at }]
const tags = await client.tags.list(); // [{ id, name, links_count, created_at }]const usage = await client.account.usage();
console.log(usage.plan); // "pro"
console.log(usage.usage); // { links_created, human_clicks, api_requests }
console.log(usage.limits); // { links_per_month, human_clicks_per_month, api_requests_per_minute }Webhook helpers use node:crypto and ship as their own entry point,
inbio/webhooks, so browser bundles of the core client stay Node-free.
Verify the exact raw request body — never re-serialized JSON:
import express from "express";
import { verify, SignatureVerificationError } from "inbio-sdk/webhooks";
const app = express();
app.post("/webhooks/inbio", express.raw({ type: "application/json" }), (req, res) => {
try {
const event = verify(req.body, req.header("X-Inbio-Signature"), process.env.INBIO_WEBHOOK_SECRET!);
if (event.event === "link.clicked") {
console.log("click on", event.data);
}
res.sendStatus(200);
} catch (err) {
if (err instanceof SignatureVerificationError) return res.sendStatus(400);
throw err;
}
});verify(payload, signatureHeader, secret, { tolerance }) recomputes
HMAC-SHA256(secret, "<t>.<raw body>"), compares it in constant time and rejects
timestamps older than tolerance seconds (default 300) to prevent replay.
constructEvent is an alias of verify.
Every error extends InbioError (with status, errorType and the raw response
body), so one catch can branch on the subclass:
| Class | When | Extra fields |
|---|---|---|
AuthenticationError |
401 — missing/invalid token | |
AccessError |
403 — plan, scope or account restriction | errorType (plan/scope/account), required scope |
NotFoundError |
404 — not found or not yours | |
ValidationError |
422 — invalid input | errors per-field map |
EntitlementError |
422 with error.type = "entitlement" — plan feature/limit |
|
StateConflictError |
409 — invalid state transition | current status |
RateLimitError |
429 | retryAfter seconds |
ServerError |
5xx | |
SignatureVerificationError |
webhook verification failed |
import { RateLimitError, ValidationError } from "inbio-sdk";
try {
await client.links.create("https://example.com", { slug: "x" });
} catch (err) {
if (err instanceof ValidationError) console.error(err.errors); // { slug: ["..."] }
else if (err instanceof RateLimitError) await sleep(err.retryAfter! * 1000);
else throw err;
}const client = new Inbio({
token: "...",
baseUrl: "https://in.bio", // default
timeout: 30_000, // ms, default 30s
maxRetries: 2, // default
});Only idempotent GET requests and 429 responses that carry Retry-After are
retried, with exponential backoff capped at 10 seconds. Set maxRetries: 0 to
disable retries. A custom fetch implementation can be injected via the fetch
option.
When no token is passed, the client reads INBIO_API_TOKEN:
export INBIO_API_TOKEN="your-token"const client = new Inbio(); // uses INBIO_API_TOKENFull API documentation: docs.in.bio
License: MIT
INBIO (in.bio) is a URL shortener and link-management
platform: short links with custom slugs, real-time click analytics
(countries, devices, browsers, referrers — bots filtered out), a QR code
studio with dot styles, marker shapes and colors, folders, tags, UTM
tools, and a REST API with webhooks. Free plan included.
- Website: https://in.bio
- Documentation: https://docs.in.bio
- Free shorten API (no key): https://docs.in.bio/api/free-shorten
- Free QR code API (no key): https://docs.in.bio/api/free-qr
- MCP server for AI agents: https://docs.in.bio/api/mcp
- All SDKs (JavaScript, Python, PHP, Ruby, Go): https://docs.in.bio/sdks
MIT © InBio, Inc.