Reference — SDK
SDK reference
getalinky ships two entry points: a pair of top-level convenience functions for one-shot creates and updates, and a full LinkyClient class that mirrors every authed HTTP route. Both are plain JS with zero runtime dependencies; the client uses globalThis.fetch.
Install
npm install getalinkyPick your entry point
| Import | Use when |
|---|---|
import { createLinky } from "getalinky" | One-shot creates (anonymous or authed). Zero-config: no client instance, no plumbing. |
import { updateLinky } from "getalinky" | Top-level convenience for a single update call. Requires apiKey in options. |
import { LinkyClient } from "getalinky/sdk" | Anything beyond create/update — getLinky, listLinkies, getInsights, key management, typed LinkyApiError, reusable config. This is the full surface. |
LinkyClient — class shape
import { LinkyClient, LinkyApiError } from "getalinky/sdk";
export class LinkyClient {
constructor(options?: {
baseUrl?: string; // defaults to $LINKY_BASE_URL, then https://getalinky.com
apiKey?: string; // defaults to $LINKY_API_KEY
client?: string; // Linky-Client header; convention <tool>/<version>
fetchImpl?: typeof fetch;
});
// Linkies
createLinky(input): Promise<CreateLinkyResponseDto>;
getLinky(slug): Promise<LinkyDto>;
listLinkies(params?): Promise<LinkyListResponseDto>;
updateLinky(slug, patch): Promise<UpdateLinkyResponseDto>;
deleteLinky(slug): Promise<DeleteLinkyResponseDto>;
getVersions(slug): Promise<LinkyVersionsResponseDto>;
getInsights(slug, params?): Promise<LauncherInsightsDto>;
// Auth + keys (require keys:admin)
whoami(): Promise<KeyListResponseDto>;
listKeys(): Promise<KeyListResponseDto>;
createKey(input): Promise<CreatedKeyResponseDto>;
revokeKey(id): Promise<RevokedKeyResponseDto>;
}Every method returns a typed DTO or throws LinkyApiError. The client reuses config across calls — instantiate once per process, not per request.
LinkyClient — common patterns
import { LinkyClient } from "getalinky/sdk";
const linky = new LinkyClient({
apiKey: process.env.LINKY_API_KEY,
client: "release-bot/1.0",
});
// Create
const { slug, url } = await linky.createLinky({
urls: ["https://linear.app/acme", "https://github.com/acme/pulls"],
title: "Release review",
});
// Read
const detail = await linky.getLinky(slug);
const page = await linky.listLinkies({ limit: 20, offset: 0 });
// Update
await linky.updateLinky(slug, {
title: "Release review — v2",
});
// Insights (Sprint 2.7)
const insights = await linky.getInsights(slug, { range: "7d" });
console.log(insights.totals); // { views, uniqueViewerDays, openAllClicks, openAllRate }
// Delete (admin role on org-owned Linkies)
await linky.deleteLinky(slug);LinkyClient — key management
Requires a bearer with the keys:admin scope. Keys are owned by the calling subject (user or active org); org-admin role is additionally required for org-owned keys. See Access control.
import { LinkyClient } from "getalinky/sdk";
const linky = new LinkyClient({ apiKey: process.env.LINKY_API_KEY });
// Mint a narrow, rate-limited key for an agent. RAW KEY IS SHOWN ONCE.
const { apiKey, rawKey, warning } = await linky.createKey({
name: "agent-release-notes",
scopes: ["links:read"], // narrowest scope — safe for LLM context
rateLimitPerHour: 200, // 0 = unlimited (reserve for internal)
});
console.warn(warning);
console.log(rawKey); // persist immediately
// List (revoked keys included, with revokedAt set)
const { apiKeys } = await linky.listKeys();
// Revoke by numeric id
await linky.revokeKey(apiKey.id);LinkyClient — error handling
Every non-2xx response throws a LinkyApiError with a stable code you can switch on without string-matching the message.
import { LinkyClient, LinkyApiError } from "getalinky/sdk";
const linky = new LinkyClient({ apiKey: process.env.LINKY_API_KEY });
try {
await linky.updateLinky("abc123", { title: "new" });
} catch (error) {
if (error instanceof LinkyApiError) {
// error.code — server-stable string: "FORBIDDEN", "NOT_FOUND",
// "BAD_REQUEST", "RATE_LIMITED", "UNAUTHORIZED", etc.
// error.statusCode — HTTP status from the response.
// error.details — optional structured payload on BAD_REQUEST.
// error.retryAfterSeconds — present on RATE_LIMITED (Sprint 2.8 Chunk D).
if (error.code === "RATE_LIMITED") {
await sleep((error.retryAfterSeconds ?? 60) * 1000);
// …retry
}
}
throw error;
}Top-level wrapper — types
export type CreateLinkyOptions = {
urls: string[];
baseUrl?: string;
source?: string;
metadata?: Record<string, unknown>;
email?: string;
title?: string;
description?: string;
urlMetadata?: UrlMetadata[];
client?: string;
resolutionPolicy?: ResolutionPolicy;
fetchImpl?: typeof fetch;
};
export type CreateLinkyResult = {
slug: string;
url: string;
claimUrl?: string;
claimToken?: string;
claimExpiresAt?: string;
warning?: string;
resolutionPolicy?: ResolutionPolicy;
};
export type UpdateLinkyOptions = {
slug: string;
baseUrl?: string;
title?: string | null;
description?: string | null;
urls?: string[];
urlMetadata?: UrlMetadata[];
resolutionPolicy?: ResolutionPolicy | null;
client?: string;
apiKey?: string;
fetchImpl?: typeof fetch;
};
export type UpdateLinkyResult = {
slug: string;
urls: string[];
urlMetadata: UrlMetadata[];
title: string | null;
description: string | null;
resolutionPolicy?: ResolutionPolicy;
updatedAt?: string;
};DSL types (ResolutionPolicy, PolicyRule, PolicyCondition, PolicyViewerField) ship with the package, so your editor gets full autocomplete on policy objects with no extra install.
Top-level wrapper — options
| Option | Type | Notes |
|---|---|---|
urls | string[] | Required. Same constraints as the API. |
baseUrl | string | Defaults to $LINKY_BASE_URL if set, otherwise https://getalinky.com. |
source | string | Free-form caller label for ops. |
title, description | string | Optional labels. |
urlMetadata | UrlMetadata[] | Optional per-URL notes / tags / openPolicy aligned with urls. |
email | string | Anonymous only. Flags the claim token for the named recipient. |
client | string | Linky-Client header value. Convention: <tool>/<version>. |
resolutionPolicy | ResolutionPolicy | Optional. Lock the Linky down from the first click. See Personalize. |
fetchImpl | typeof fetch | Override for tests or non-global-fetch runtimes. Defaults to globalThis.fetch. |
apiKey | string | Required for updateLinky(). Bearer token created from the dashboard's API-keys page. User-scoped keys edit personal launch bundles; org-scoped keys edit team-owned bundles. Keys carry one of three scopes — links:read, links:write, keys:admin — locked at mint. A links:read key cannot call updateLinky(); pick links:write or higher in the dashboard when minting. See Access control. |
metadata | Record<string, unknown> | Free-form caller metadata. _linky.* is server-reserved and stripped. |
Top-level wrapper — result
claimUrl, claimToken, claimExpiresAt, and warning are present only on anonymous creates. resolutionPolicyis present when a policy was attached at create time — the server echoes the parsed form (with minted rule ids) so you don't need a second fetch.
updateLinky() returns the updated Linky shape (slug, urls, metadata, title, description, policy, updatedAt). Policy clears use resolutionPolicy: null.
Top-level wrapper — basic create
const { createLinky } = require("getalinky");
const result = await createLinky({
urls: ["https://example.com", "https://example.org"],
source: "agent",
title: "Release review",
});
console.log(result.url);
if (result.claimUrl) {
console.warn(result.warning);
console.log(result.claimUrl);
}Top-level wrapper — create with policy
const { createLinky } = require("getalinky");
await createLinky({
urls: ["https://acme.com/docs", "https://acme.com/status"],
source: "agent",
title: "Acme standup",
email: "alice@acme.com", // lands the claim URL with a human
resolutionPolicy: {
version: 1,
rules: [
{
name: "Engineering team",
when: { op: "endsWith", field: "emailDomain", value: "acme.com" },
tabs: [{ url: "https://linear.app/acme/my-issues" }],
},
],
},
});Top-level wrapper — authenticated update
const { updateLinky } = require("getalinky");
await updateLinky({
slug: "abc123",
apiKey: process.env.LINKY_API_KEY,
title: "Release bundle v2",
resolutionPolicy: {
version: 1,
rules: [
{
name: "Engineering team",
when: { op: "endsWith", field: "emailDomain", value: "acme.com" },
tabs: [{ url: "https://linear.app/acme/my-issues" }],
},
],
},
});Rate limits
Every authenticated request counts against the key's per-hour bucket (default 1000/hour, configurable at mint time). When a bucket is exhausted the SDK throws LinkyApiError with code: "RATE_LIMITED" and retryAfterSeconds. See Limits for the full picture.