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
| Method | HTTP | Returns |
|---|---|---|
links.create(params) | POST /links | full Link |
links.get(id) | GET /links/:id | full Link |
links.list(params?) | GET /links | LinkPage |
links.update(id, params) | PATCH /links/:id | full Link |
links.delete(id) | DELETE /links/:id | { deleted: true } |
links.qrSvg(id) | GET /links/:id/qr.svg | SVG markup string |
links.qrPng(id) | GET /links/:id/qr.png | PNG bytes (Buffer) |
links.listAll(params?) | walks GET /links | AsyncGenerator<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.
Link
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.
Creating links
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://…" },
});
Updating links
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 passesnullthrough verbatim — it is never dropped. Withog, a field-levelnullclears 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).
Deleting links
await client.links.delete(link.id); // resolves { deleted: true }