Linky logoLinky
Docs navigation

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 getalinky

Pick your entry point

ImportUse 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

OptionTypeNotes
urlsstring[]Required. Same constraints as the API.
baseUrlstringDefaults to $LINKY_BASE_URL if set, otherwise https://getalinky.com.
sourcestringFree-form caller label for ops.
title, descriptionstringOptional labels.
urlMetadataUrlMetadata[]Optional per-URL notes / tags / openPolicy aligned with urls.
emailstringAnonymous only. Flags the claim token for the named recipient.
clientstringLinky-Client header value. Convention: <tool>/<version>.
resolutionPolicyResolutionPolicyOptional. Lock the Linky down from the first click. See Personalize.
fetchImpltypeof fetchOverride for tests or non-global-fetch runtimes. Defaults to globalThis.fetch.
apiKeystringRequired 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.
metadataRecord<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.