TypeScript SDK
BetaWork in Progress
This SDK is currently in beta and under active development. It is not recommended for production use as APIs and functionality may change without notice.
Current Status:
- Designed for server-side use in Node.js and similar TypeScript runtimes
- Links, analytics, domains, members, teams, and bootstrap info are documented below
- Examples on this page are aligned to the current
@fynlink/sdkAPI shape - Local example scripts remain the fastest way to validate behavior against your own credentials
For the latest updates and changes, please check the GitHub repository.
Full operation-by-operation SDK response samples are documented in the fyn-ts README and examples README.
Installation
Install the SDK using your preferred package manager:
npm install @fynlink/sdkyarn add @fynlink/sdkpnpm add @fynlink/sdkSDK Dependencies
The SDK installs these external dependencies automatically:
axios- HTTP client for API requestsnanoid- cryptographically secure slug generation used in@fynlink/corelibsodium-wrappers-sumo- cryptographic operations (via@fynlink/crypto)
Note: npm, yarn, and pnpm resolve and install these dependencies automatically. No manual installation is required.
Environment Setup
The local examples in fyn-ts/examples read credentials from examples/.env.
cp examples/.env.example examples/.env
FYN_API_TOKEN=...
FYN_SECRET_KEY=...
FYN_API_URL=https://api.fyn.link/v1
FYN_DEBUG=false
FYN_CACHE_ENABLED=true
FYN_CACHE_TTL_MS=300000
FYN_CACHE_STORAGE=disk
FYN_CACHE_PATH=.fynlink-sdk-cache.json
FYN_CACHE_WARN_ON_DISK=trueNote: FYN_SECRET_KEY is required for encrypted link reads, lists, and updates.
Cache security: Disk cache stores raw API responses. Link payload fields remain encrypted envelopes, but metadata and headers are cached as returned. If both cache data and FYN_SECRET_KEY are exposed, encrypted link fields may be decrypted.
Quick Start
Get started with the Fynlink SDK by following these examples:
Initialize Client
⚠️ Security Warning
This SDK is designed for server-side use only. Never expose your token or secret in client-side code or public repositories. Always store these credentials in secure environment variables on your server.
import { Fyn } from '@fynlink/sdk';
const fyn = new Fyn({
token: process.env.FYN_API_TOKEN!,
secret: process.env.FYN_SECRET_KEY
});
const link = await fyn.links.create({
domain: 'go.example.com',
target: 'https://example.com',
title: 'Hello from FynLink'
});
console.log(link.shortUrl);import { AnalyticsMode, Fyn } from '@fynlink/sdk';
const fyn = new Fyn({
token: process.env.FYN_API_TOKEN!,
secret: process.env.FYN_SECRET_KEY,
baseUrl: process.env.FYN_API_URL,
timeoutMs: 10_000,
debug: false,
retry: {
maxAttempts: 2,
initialDelayMs: 1_000
},
cache: {
enabled: true,
ttlMs: 300_000,
storage: 'disk',
path: '.fynlink-sdk-cache.json',
warnOnDisk: true
},
defaults: {
links: {
create: {
domain: 'go.example.com',
safeMode: true,
analytics: AnalyticsMode.Full,
privateLink: false,
expirySeconds: 86_400
}
}
}
});Configuration Reference
Required Options
tokenAPI token used for authenticated requests.
secretRequired for encrypted link reads, lists, and updates.
Optional Options
baseUrlCustom API base URL. Default: https://api.fyn.link/v1
timeoutMsRequest timeout in milliseconds. Default: 10000
debugEnables HTTP and cache diagnostics. Default: false
Retry Options
retry.maxAttemptsMaximum retry attempts. Default: 2
retry.initialDelayMsInitial retry delay in milliseconds. Default: 1000
Cache Options
cache.enabledEnable or disable caching. Default: true
cache.ttlMsCache TTL in milliseconds. Default: 300000
cache.storageCache backend. Default: 'disk'
cache.pathDisk cache file path. Default: .fynlink-sdk-cache.json
cache.warnOnDiskEmit a disk-cache security warning. Default: true
Disk cache note: Cached link payload fields are encrypted envelopes, but metadata/headers are stored as returned. If both cache data and your secret key are exposed, encrypted link fields may be decrypted.
Create Defaults
These are SDK-level defaults. Team defaults from Fynlink settings are not auto-applied during SDK link creation.
defaults.links.create.domainDefault domain for fyn.links.create(...) when per-request domain is omitted.
defaults.links.create.safeModeDefault safe-mode value for link creation.
defaults.links.create.analyticsDefault analytics mode for link creation.
defaults.links.create.privateLinkDefault privacy mode for link creation.
defaults.links.create.expirySecondsDefault TTL in seconds for link creation.
Links
Create Link
Important: Team defaults set in the web app (such as default domain, private link mode, safe mode, analytics mode, and expiry) are not automatically respected by the SDK when creating links. Set values per request or configure defaults.links.create during new Fyn(...).
Basic Usage
Create a simple link with the same shape used by the local beta examples.
const link = await fyn.links.create({
domain: 'staging.fyn.li',
target: 'https://example.com',
title: 'Example Link',
notes: 'Created from SDK docs example',
tags: ['example', 'beta'],
safeMode: true
});| Field | Type | Required | Description |
|---|---|---|---|
domain | string | Conditionally | Domain used for the short link. Required unless defaults.links.create.domain is set in new Fyn(...). |
slug | string | No | Custom slug. Auto-generated when omitted. |
target | string | Yes | Default destination URL. |
title | string | No | Human-readable title. |
notes | string | No | Internal notes. |
tags | string[] | No | Arbitrary tags for organization. |
iosTarget | string | No | iOS-specific destination URL. |
androidTarget | string | No | Android-specific destination URL. |
geoTargets | Record<string, string> | No | Country-specific destination URLs keyed by country code. |
password | string | No | Password required to access the link. |
safeMode | boolean | No | Safe mode flag. Falls back to SDK default (true) when omitted. |
privateLink | boolean | No | Marks the link as private. |
analytics | AnalyticsMode | No | Analytics level. Falls back to SDK default (AnalyticsMode.Full) when omitted. |
expirySeconds | number | No | Time to live in seconds. |
import { AnalyticsMode, Fyn } from '@fynlink/sdk';
const fyn = new Fyn({
token: process.env.FYN_API_TOKEN!,
defaults: {
links: {
create: {
domain: 'staging.fyn.li',
safeMode: true,
analytics: AnalyticsMode.Full,
privateLink: false,
expirySeconds: 86_400
}
}
}
});
const link = await fyn.links.create({
target: 'https://example.com'
});{
"id": "link_123",
"userId": "user_99",
"teamId": "team_7",
"shortUrl": "https://staging.fyn.li/Ab12Cd34",
"domain": "staging.fyn.li",
"slug": "Ab12Cd34",
"target": {
"default": "https://example.com",
"ios": null,
"android": null,
"geo": {}
},
"title": "Example Link",
"notes": "Created from SDK docs example",
"tags": ["example", "beta"],
"isPrivate": false,
"safeMode": true,
"viaApi": true,
"passwordEnabled": false,
"clicks": 0,
"createdAt": "2026-03-21T10:22:00.000000Z",
"updatedAt": "2026-03-21T10:22:00.000000Z",
"expiresAt": null,
"tracking": "full",
"status": "active"
}Advanced Usage
Create a link with analytics mode, platform targets, geo routing, privacy, and expiration.
import { AnalyticsMode } from '@fynlink/sdk';
const link = await fyn.links.create({
domain: 'go.example.com',
slug: 'launch-2026',
target: 'https://example.com',
title: 'Launch Page',
notes: 'Primary campaign link',
tags: ['launch', 'campaign'],
iosTarget: 'https://apps.apple.com/app/id123456789',
androidTarget: 'https://play.google.com/store/apps/details?id=com.example.app',
geoTargets: {
US: 'https://us.example.com',
IN: 'https://in.example.com'
},
password: 'optional-password',
safeMode: true,
privateLink: false,
analytics: AnalyticsMode.Full,
expirySeconds: 86400
});Available analytics modes: AnalyticsMode.None, AnalyticsMode.Full, AnalyticsMode.Partial, AnalyticsMode.Clicks
Get Link by ID
const link = await fyn.links.get('link_123');
console.log(link.id);
console.log(link.shortUrl);
console.log(link.target?.default);interface LinkRecord {
id: string;
userId: string;
teamId: string;
domain: string | null;
slug: string | null;
shortUrl: string | null;
target: {
default: string | null;
ios: string | null;
android: string | null;
geo: Record<string, string>;
options?: { password?: string | null };
} | null;
title: string | null;
notes: string | null;
tags: string[];
isPrivate: boolean;
safeMode: boolean;
viaApi: boolean;
passwordEnabled: boolean;
clicks: number;
createdAt: string;
updatedAt: string;
expiresAt: string | null;
tracking: AnalyticsMode;
status: string | null;
}{
"id": "link_123",
"userId": "user_99",
"teamId": "team_7",
"domain": "go.example.com",
"slug": "launch-2026",
"shortUrl": "https://go.example.com/launch-2026",
"target": {
"default": "https://example.com",
"ios": null,
"android": null,
"geo": {}
},
"title": "Launch Page",
"notes": "Primary campaign link",
"tags": ["launch", "campaign"],
"isPrivate": false,
"safeMode": true,
"viaApi": true,
"passwordEnabled": false,
"clicks": 42,
"createdAt": "2026-03-20T10:15:30.000000Z",
"updatedAt": "2026-03-21T09:00:00.000000Z",
"expiresAt": null,
"tracking": "full",
"status": "active"
}List Links
Basic Usage
List links with pagination metadata.
const links = await fyn.links.list({
limit: 10,
sortBy: 'created_at',
sortOrder: 'desc'
});| Field | Type | Description |
|---|---|---|
page | number | Page number. |
limit | number | Page size. |
sortBy | 'created_at' | 'updated_at' | 'clicks' | Sort field. |
sortOrder | 'asc' | 'desc' | Sort direction. |
filters | LinkListFilter[] | Filter rules applied by the API. |
{
"data": [
{
"id": "link_123",
"userId": "user_99",
"teamId": "team_7",
"domain": "go.example.com",
"slug": "launch-2026",
"shortUrl": "https://go.example.com/launch-2026",
"target": {
"default": "https://example.com",
"ios": null,
"android": null,
"geo": {}
},
"title": "Launch Page",
"notes": "Primary campaign link",
"tags": ["launch", "campaign"],
"isPrivate": false,
"safeMode": true,
"viaApi": true,
"passwordEnabled": false,
"status": "active",
"clicks": 42,
"createdAt": "2026-03-20T10:15:30.000000Z",
"updatedAt": "2026-03-21T09:00:00.000000Z",
"expiresAt": null,
"tracking": "full"
}
],
"meta": {
"current_page": 1,
"from": 1,
"to": 1,
"per_page": 10,
"last_page": 1,
"total": 1
}
}Advanced Usage
Filter links by domain, tracking mode, privacy, safe mode, and status.
import { AnalyticsMode, LinkStatus } from '@fynlink/sdk';
const links = await fyn.links.list({
page: 1,
limit: 25,
sortBy: 'created_at',
sortOrder: 'desc',
filters: [
{
domain: 'go.example.com',
tracking: AnalyticsMode.Full,
safeMode: true,
isPrivate: false,
password: false,
status: LinkStatus.Active
}
]
});Available link statuses: active, inactive, flagged, reported, reported_banned, needs_upgrade, quarantined
Note: viaApi is backend-managed response metadata on LinkRecord, not a list filter input.
Delete Link
await fyn.links.delete('link_123');
console.log('Deleted link_123');Possible SDK output: the promise resolves with no return value (void).
Update Link
const updated = await fyn.links.update({
id: 'link_123',
title: 'Updated Example Link',
notes: 'Updated from SDK docs example',
target: 'https://example.org/new',
safeMode: true,
password: null,
expirySeconds: null
});{
"id": "link_123",
"userId": "user_99",
"teamId": "team_7",
"domain": "go.example.com",
"slug": "launch-2026",
"shortUrl": "https://go.example.com/launch-2026",
"target": {
"default": "https://example.org/new",
"ios": null,
"android": null,
"geo": {}
},
"title": "Updated Example Link",
"notes": "Updated from SDK docs example",
"tags": ["launch", "campaign"],
"isPrivate": false,
"safeMode": true,
"viaApi": true,
"passwordEnabled": false,
"clicks": 42,
"createdAt": "2026-03-20T10:15:30.000000Z",
"updatedAt": "2026-03-21T10:22:00.000000Z",
"expiresAt": null,
"tracking": "full",
"status": "active"
}Tip: Use password: null to remove a password and expirySeconds: null to clear expiration.
Domains
List Domains
const domains = await fyn.domains.list({
page: 1,
limit: 10
});| Field | Type | Description |
|---|---|---|
page | number | Page number. |
limit | number | Page size. |
{
"data": [
{
"id": "domain_123",
"domain": "go.example.com",
"description": "Primary branded short domain",
"isSubdomain": true,
"userId": "user_99",
"teams": ["team_7"],
"status": "pending",
"notFoundUrl": "https://example.com/404",
"rootRedirect": "https://example.com",
"gpc": false,
"createdAt": "2026-03-20T09:00:00.000000Z",
"updatedAt": "2026-03-20T09:00:00.000000Z"
}
],
"meta": {
"current_page": 1,
"from": 1,
"to": 1,
"per_page": 10,
"last_page": 1,
"total": 1
}
}Get Domain
const domain = await fyn.domains.get('domain_123');
console.log(domain.domain);
console.log(domain.status);{
"id": "domain_123",
"domain": "go.example.com",
"description": "Primary branded short domain",
"isSubdomain": true,
"userId": "user_99",
"teams": ["team_7"],
"status": "pending",
"notFoundUrl": "https://example.com/404",
"rootRedirect": "https://example.com",
"gpc": false,
"createdAt": "2026-03-20T09:00:00.000000Z",
"updatedAt": "2026-03-20T09:00:00.000000Z"
}Create Domain
const created = await fyn.domains.create({
domain: 'go.example.com',
description: 'Primary branded short domain',
notFoundUrl: 'https://example.com/404',
rootRedirect: 'https://example.com',
gpc: false
});| Field | Type | Required | Description |
|---|---|---|---|
domain | string | Yes on create | Domain name to register. |
description | string | No | Optional description. |
notFoundUrl | string | No | URL used for unmatched slugs. |
rootRedirect | string | No | URL used for the bare domain root. |
gpc | boolean | No | Global privacy control setting. |
Team assignment: You do not pass a team when creating a domain. The backend automatically assigns the domain to the current team associated with the token used for the request.
{
"id": "domain_123",
"domain": "go.example.com",
"description": "Primary branded short domain",
"isSubdomain": true,
"userId": "user_99",
"teams": ["team_7"],
"status": "pending",
"notFoundUrl": "https://example.com/404",
"rootRedirect": "https://example.com",
"gpc": false,
"createdAt": "2026-03-20T09:00:00.000000Z",
"updatedAt": "2026-03-20T09:00:00.000000Z"
}Update Domain
const updated = await fyn.domains.update({
id: 'domain_123',
description: 'Updated from SDK docs example',
notFoundUrl: 'https://example.com/not-found',
rootRedirect: 'https://example.com/home',
gpc: true
});| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Domain ID to update. |
description | string | No | Updated description. |
notFoundUrl | string | No | Updated not found URL. |
rootRedirect | string | No | Updated root redirect URL. |
gpc | boolean | No | Updated global privacy control setting. |
Team ownership: Domain team association is not changed through update; this PATCH only updates mutable domain settings.
{
"id": "domain_123",
"domain": "go.example.com",
"description": "Updated from SDK docs example",
"isSubdomain": true,
"userId": "user_99",
"teams": ["team_7"],
"status": "active",
"notFoundUrl": "https://example.com/not-found",
"rootRedirect": "https://example.com/home",
"gpc": true,
"createdAt": "2026-03-20T09:00:00.000000Z",
"updatedAt": "2026-03-21T08:30:00.000000Z"
}Verify Domain
const verification = await fyn.domains.verify('domain_123');
console.log(verification.verified);
console.log(verification.cname);{
"id": "domain_123",
"domain": "go.example.com",
"status": "active",
"verified": true,
"cname": "cname.fyn.link"
}Delete Domain
await fyn.domains.remove('domain_123');
console.log('Removed domain_123');Possible SDK output: the promise resolves with no return value (void).
Team
List Members
const members = await fyn.members.list({
includeOwner: true
});{
"data": [
{
"id": "user_123",
"name": "Alex Doe",
"email": "member@example.com",
"role": "editor",
"displayPicture": "https://cdn.example.com/avatar/alex.png",
"teamId": "team_7",
"createdAt": "2026-03-01T11:00:00.000000Z",
"updatedAt": "2026-03-15T09:00:00.000000Z"
}
],
"meta": {
"current_page": 1,
"from": 1,
"to": 1,
"per_page": 15,
"last_page": 1,
"total": 1
}
}Invite Member
await fyn.members.invite({
email: 'member@example.com',
role: 'editor'
});Security note: Only already-registered users can be invited, by design, to preserve the platform’s security architecture.
Possible SDK output: the promise resolves with no return value (void).
Get Member
const member = await fyn.members.get('member_123');
console.log(member.email);
console.log(member.role);{
"id": "user_123",
"name": "Alex Doe",
"email": "member@example.com",
"role": "editor",
"displayPicture": "https://cdn.example.com/avatar/alex.png",
"teamId": "team_7",
"createdAt": "2026-03-01T11:00:00.000000Z",
"updatedAt": "2026-03-15T09:00:00.000000Z"
}Update Permissions
const updatedMember = await fyn.members.update({
id: 'member_123',
role: 'admin'
});
const currentTeam = await fyn.teams.get();
const updatedTeam = await fyn.teams.update({
name: 'Growth Team'
});// fyn.members.update(...)
{
"id": "user_123",
"name": "Alex Doe",
"email": "member@example.com",
"role": "admin",
"displayPicture": "https://cdn.example.com/avatar/alex.png",
"teamId": "team_7",
"createdAt": "2026-03-01T11:00:00.000000Z",
"updatedAt": "2026-03-21T10:00:00.000000Z"
}
// fyn.teams.get() and fyn.teams.update(...)
{
"id": "team_7",
"type": "team",
"attributes": {
"name": "Growth Team",
"privateLinks": false,
"safeMode": true,
"tracking": "full"
},
"owner": {
"id": "user_99",
"type": "user",
"name": "Owner Name"
},
"defaultDomain": {
"id": "domain_123",
"value": "go.example.com"
},
"createdAt": "2026-01-01T00:00:00+00:00",
"updatedAt": "2026-03-21T10:15:00+00:00"
}The current SDK exposes member role updates via fyn.members.update() and team name updates via fyn.teams.update().
Remove Member
await fyn.members.remove('member_123');
console.log('Removed member_123');Possible SDK output: the promise resolves with no return value (void).
Analytics
Link Analytics
const clicks = await fyn.analytics.clicks('link_123');
const analytics = await fyn.analytics.timeseries('link_123', {
granularity: 'day'
});Available methods: clicks(), timeseries(), browsers(), devices(), countries(), referrers()
{
"clicks": {
"id": "link_123",
"clicks": 120
},
"timeseries": {
"data": [
{
"date": "2026-03-01T00:00:00+00:00",
"clicks": 14,
"uniqueClicks": 12
}
],
"meta": {
"totalClicks": 120,
"totalUniqueClicks": 95,
"count": 10
}
}
}Breakdowns
const browsers = await fyn.analytics.browsers('link_123', {
granularity: 'day'
});
const devices = await fyn.analytics.devices('link_123', {
granularity: 'day'
});
const countries = await fyn.analytics.countries('link_123', {
granularity: 'day'
});
const referrers = await fyn.analytics.referrers('link_123', {
granularity: 'day'
});{
"browsers": {
"data": [
{ "browser": "Chrome", "clicks": 58, "percentage": 48.33 }
],
"meta": { "totalClicks": 120, "count": 1 }
},
"devices": {
"data": [
{ "device": "desktop", "clicks": 73, "percentage": 60.83 }
],
"meta": { "totalClicks": 120, "count": 1 }
},
"countries": {
"data": [
{ "country": "United States", "countryCode": "US", "clicks": 64, "percentage": 53.33 }
],
"meta": { "totalClicks": 120, "count": 1, "countriesCount": 1 }
},
"referrers": {
"data": [
{ "referrer": "google.com", "clicks": 66, "percentage": 55.0 }
],
"meta": { "totalClicks": 120, "count": 1, "sourcesCount": 1 }
}
}Metrics Query
const stats = await fyn.analytics.timeseries('link_123', {
start: '2026-03-01T00:00:00.000Z',
end: '2026-03-19T23:59:59.999Z',
granularity: 'day',
country: ['US', 'IN'],
browser: ['Chrome', 'Safari'],
device: ['desktop', 'mobile'],
referrer: ['google.com']
});| Field | Type | Description |
|---|---|---|
start | string | Start timestamp. |
end | string | End timestamp. |
granularity | 'minute' | 'hour' | 'day' | 'week' | 'month' | Aggregation granularity. |
country | string[] | Country filters. |
browser | string[] | Browser filters. |
device | string[] | Device filters. |
referrer | string[] | Referrer filters. |
{
"data": [
{
"date": "2026-03-01T00:00:00+00:00",
"clicks": 14,
"uniqueClicks": 12
}
],
"meta": {
"totalClicks": 120,
"totalUniqueClicks": 95,
"count": 10
}
}Bootstrap & Commands
const info = await fyn.info.get();
const freshInfo = await fyn.info.get(true);
console.log(info.team.defaultDomain.value);
console.log(freshInfo.permissions);npx tsx examples/analytics/clicks.ts <linkId>
npx tsx examples/analytics/timeseries.ts <linkId>
npx tsx examples/analytics/browsers.ts <linkId>
npx tsx examples/analytics/devices.ts <linkId>
npx tsx examples/analytics/countries.ts <linkId>
npx tsx examples/analytics/referrers.ts <linkId>
npx tsx examples/info/get.ts{
"id": "token_abc",
"user": { "id": "user_99", "name": "Owner Name" },
"team": {
"id": "team_7",
"name": "Growth Team",
"publicKey": "base64-public-key",
"disableAnalytics": false,
"safeMode": true,
"privateLink": false,
"defaultDomain": { "id": "domain_123", "value": "go.example.com" },
"availableDomains": [
{ "id": "domain_123", "value": "go.example.com" },
{ "id": "domain_124", "value": "links.example.com" }
]
},
"permissions": ["link.read", "link.create", "member.read"],
"tokenName": "SDK Token"
}Features
Explore the practical capabilities of the TypeScript SDK:
Automatic Retries
Built-in retry handling for transient failures using the configured retry policy.
const fyn = new Fyn({
token: process.env.FYN_API_TOKEN!,
retry: {
maxAttempts: 2,
initialDelayMs: 1000
}
});
const link = await fyn.links.get('link_123');Caching Strategy
ETag-aware caching for GET requests, with memory or disk-backed storage and explicit disk-cache security warnings.
const fyn = new Fyn({
token: process.env.FYN_API_TOKEN!,
secret: process.env.FYN_SECRET_KEY,
cache: {
enabled: true,
ttlMs: 60000,
storage: 'disk',
path: '.fynlink-sdk-cache.json',
warnOnDisk: true
}
});Bootstrap Info
Fetch the current token, user, team, permissions, and available domains.
const info = await fyn.info.get();
const freshInfo = await fyn.info.get(true);
console.log(info.team.name);
console.log(freshInfo.permissions);Type Definitions
The SDK ships with strong TypeScript definitions and re-exports the core types from @fynlink/core.
import type {
AnalyticsMode,
CreateDomainInput,
CreateLinkInput,
FynBootstrapInfo,
FynClientConfig,
LinkRecord,
ListLinksResult,
MetricsQuery,
TeamRecord
} from '@fynlink/sdk';
const config: FynClientConfig = {
token: process.env.FYN_API_TOKEN!,
secret: process.env.FYN_SECRET_KEY
};
const createInput: CreateLinkInput = {
domain: 'go.example.com',
target: 'https://example.com'
};
type Link = LinkRecord;
type Team = TeamRecord;
type Query = MetricsQuery;
type Mode = AnalyticsMode;
type Bootstrap = FynBootstrapInfo;
type LinksResult = ListLinksResult;
type DomainCreate = CreateDomainInput;