Skip to main content

API reference

@optomatica/optolink-sdk exposes a single client with one resource, client.links — seven methods plus the listAll pagination helper. Constructor options are covered in install & initialize.

All methods are async and throw OptoLinkError (or OptoLinkConnectionError) on failure — see errors & retries.

Methods​

MethodHTTPReturns
links.create(params)POST /linksfull Link
links.get(id)GET /links/:idfull Link
links.list(params?)GET /linksLinkPage
links.update(id, params)PATCH /links/:idfull Link
links.delete(id)DELETE /links/:id{ deleted: true }
links.qrSvg(id)GET /links/:id/qr.svgSVG markup string
links.qrPng(id)GET /links/:id/qr.pngPNG bytes (Buffer)
links.listAll(params?)walks GET /linksAsyncGenerator<Link>

Auto-refetch: create and update follow their write with a GET /links/:id (the raw write responses are partial shapes) and resolve the uniform full Link. The extra request is hidden and shares the same retry budget as the write.

Every link-returning method resolves this shape (mirrors GET /links/:id exactly):

interface Link {
id: string;
organizationId: string;
domainId: string;
templateId: string | null;
channel: ChannelType | null;
shortCode: string;
path: string;
params: Record<string, unknown> | null;
fallbackUrl: string | null;
utmSource: string | null;
utmMedium: string | null;
utmCampaign: string | null;
utmTerm: string | null;
utmContent: string | null;
ogTitle: string | null;
ogDescription: string | null;
ogImageUrl: string | null;
title: string | null;
tags: string[];
expiry: string | null; // ISO 8601 datetime; expired links resolve 410 Gone
deferred: boolean | null; // null = inherit from template
matchWindow: number | null; // hours; null = inherit
clipboardEnabled: boolean | null; // null = inherit
isActive: boolean;
createdAt: string;
updatedAt: string;
url: string; // https://{domain}/{orgKey}/{shortCode}
domain: { domain: string; status: DomainStatus };
template?: LinkTemplateSummary | null; // present on get/refetch: { id, name, channel }
}

ChannelType is one of 'social' | 'email' | 'sms' | 'qr' | 'referral' | 'push' | 'web' | 'in_app' | 'custom'. DomainStatus is 'REQUESTED' | 'APPROVED' | 'VERIFIED' | 'REJECTED' | 'GRACE' — GRACE shows as Removing in the portal: the domain is being removed, and links keep resolving until the grace window ends.

path is the only required field; everything else is optional. The SDK does not re-validate — the backend rejects invalid input with 400 and the details land in err.details.

const link = await client.links.create({
path: "/premium/upgrade", // required: in-app destination, ≤ 2048 chars
title: "Premium upgrade", // ≤ 255 chars
channel: "social",
templateId: "…", // ≤ 100 chars
utmSource: "instagram",
utmMedium: "organic",
utmCampaign: "spring-drop",
utmTerm: "premium",
utmContent: "story-a", // utm* fields: ≤ 255 chars each
shortCode: "premium-3x9f", // ^[a-zA-Z0-9_-]{3,100}$; auto-generated when omitted
domainId: "…", // UUID of an org-owned VERIFIED domain
params: { plan: "pro" }, // deep-merged over template baseParams at read time
fallbackUrl: "https://example.com/store",
tags: ["premium"],
expiry: "2026-12-31T23:59:59Z",
deferred: true, // omit = inherit from template
matchWindow: 48, // int hours, 1–720; omit = inherit
clipboardEnabled: true, // omit = inherit
og: { title: "Go Premium", description: "…", imageUrl: "https://…" },
});

All update fields are optional. Two update semantics matter and are preserved exactly by the SDK's JSON serialization:

  • undefined → key omitted → the current value is kept. Leaving a field out never erases it.
  • JSON null → clear, back to the template's value or empty. The SDK passes null through verbatim — it is never dropped. With og, a field-level null clears just that override (og: { imageUrl: null }).
await client.links.update(link.id, {
title: "New title",
fallbackUrl: null, // clears the fallback
});

isActive?: boolean is update-only: true re-activates, false deactivates a link (deactivated links stop resolving).

Listing & pagination​

interface ListParams {
page?: number; // default 1
limit?: number; // default 20, max 100 (server-enforced)
isActive?: boolean; // filter
}

interface LinkPage {
data: Link[]; // links
total: number;
page: number;
limit: number;
}

The envelope is exactly { data, total, page, limit } — there is no hasNext or totalPages, and the SDK fabricates none. isActive is serialized as the literal strings "true"/"false" (the backend's transform maps anything else to false, so false is always sent rather than omitted).

For every link across all pages, use listAll — an async generator that walks pages starting at page 1 and stops when page × limit ≥ total:

for await (const link of client.links.listAll({ limit: 100 })) {
console.log(link.url);
}

It accepts { limit?, isActive? } and honors the server's echoed limit (relevant when the server caps your requested limit).

QR codes​

const svg = await client.links.qrSvg(link.id); // image/svg+xml markup, as text
const png = await client.links.qrPng(link.id); // PNG bytes, as a Node Buffer

Both render a fixed shape — 256×256 px, margin 2, black on white — and take no options (the API exposes none). The link must exist (404 otherwise), and responses cache for a day (Cache-Control: public, max-age=86400).

await client.links.delete(link.id); // resolves { deleted: true }