npm package: @fynlink/sdk
Description: TypeScript SDK for Fynlink
Latest version: 0.2.0-beta.5

# FynLink TypeScript SDK

Official TypeScript SDK for FynLink.

Use `@fynlink/sdk` to create and manage short links, read analytics, work with custom domains, and manage team resources from Node.js and TypeScript applications.

## Install

```bash
npm install @fynlink/sdk
```

## Quick Start

```ts
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({
  target: 'https://example.com',
  title: 'Hello from FynLink'
});

console.log(link.shortUrl);
```

## Client Setup

`new Fyn(config)` accepts the following options:

| Option | Type | Required | Description |
| --- | --- | --- | --- |
| `token` | `string` | Yes | API token used for authenticated requests. |
| `secret` | `string` | No | Required for encrypted link reads, lists, and updates. |
| `baseUrl` | `string` | No | Custom API base URL. Defaults to `https://api.fyn.link/v1`. |
| `timeoutMs` | `number` | No | Request timeout in milliseconds. Defaults to `10000`. |
| `debug` | `boolean` | No | Enables HTTP and cache diagnostics. Defaults to `false`. |
| `retry.maxAttempts` | `number` | No | Maximum retry attempts. Defaults to `2`. |
| `retry.initialDelayMs` | `number` | No | Initial retry delay in milliseconds. Defaults to `1000`. |
| `cache.enabled` | `boolean` | No | Enables response caching. Defaults to `true`. |
| `cache.ttlMs` | `number` | No | Cache TTL in milliseconds. Defaults to `300000`. |
| `cache.storage` | `'memory' \| 'disk'` | No | Cache backend. Defaults to `'disk'`. |
| `cache.path` | `string` | No | Disk cache file path. Defaults to `.fynlink-sdk-cache.json`. |
| `cache.warnOnDisk` | `boolean` | No | Emits a disk-cache security warning. Defaults to `true`. |

Example:

```ts
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
  }
});
```

## Modules

- `fyn.info`
- `fyn.links`
- `fyn.analytics`
- `fyn.domains`
- `fyn.members`
- `fyn.teams`

## Links

### Create a Link

```ts
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: 86_400
});
```

Possible SDK response:

```ts
{
  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: 'https://apps.apple.com/app/id123456789',
    android: 'https://play.google.com/store/apps/details?id=com.example.app',
    geo: {
      US: 'https://us.example.com',
      IN: 'https://in.example.com'
    },
    options: {
      password: 'optional-password'
    }
  },
  title: 'Launch Page',
  notes: 'Primary campaign link',
  tags: ['launch', 'campaign'],
  isPrivate: false,
  safeMode: true,
  viaApi: true,
  passwordEnabled: true,
  clicks: 0,
  createdAt: '2026-03-20T10:15:30.000000Z',
  updatedAt: '2026-03-20T10:15:30.000000Z',
  expiresAt: '2026-03-21T10:15:30.000000Z',
  tracking: 'full',
  status: 'active'
}
```

`fyn.links.create(input)` parameters:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `domain` | `string` | No | Domain used for the short link. Falls back to the team default domain when available. |
| `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 the team default when omitted. |
| `privateLink` | `boolean` | No | Marks the link as private. |
| `analytics` | `AnalyticsMode` | No | Analytics level. Falls back to the team default when omitted. |
| `expirySeconds` | `number` | No | Time to live in seconds. |

Available analytics modes:

- `AnalyticsMode.None`
- `AnalyticsMode.Full`
- `AnalyticsMode.Partial`
- `AnalyticsMode.Clicks`

### Get, List, Update, and Delete

```ts
import { AnalyticsMode, LinkStatus } from '@fynlink/sdk';

const link = await fyn.links.get('link_123');

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
    }
  ]
});

const updated = await fyn.links.update({
  id: 'link_123',
  target: 'https://example.com/updated',
  title: 'Updated title',
  notes: 'Updated notes',
  tags: ['updated'],
  iosTarget: 'https://ios.example.com/updated',
  androidTarget: 'https://android.example.com/updated',
  geoTargets: {
    US: 'https://us.example.com/updated'
  },
  password: null,
  safeMode: true,
  privateLink: false,
  analytics: AnalyticsMode.Clicks,
  expirySeconds: null
});

await fyn.links.delete('link_123');
```

Possible SDK response (`fyn.links.get`):

```ts
{
  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'
}
```

Possible SDK response (`fyn.links.list`):

```ts
{
  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,
      clicks: 42,
      createdAt: '2026-03-20T10:15:30.000000Z',
      updatedAt: '2026-03-21T09:00:00.000000Z',
      expiresAt: null,
      tracking: 'full',
      status: 'active'
    }
  ],
  meta: {
    current_page: 1,
    from: 1,
    to: 1,
    per_page: 25,
    last_page: 1,
    total: 1
  }
}
```

Possible SDK response (`fyn.links.update`):

```ts
{
  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/updated',
    ios: 'https://ios.example.com/updated',
    android: 'https://android.example.com/updated',
    geo: {
      US: 'https://us.example.com/updated'
    }
  },
  title: 'Updated title',
  notes: 'Updated notes',
  tags: ['updated'],
  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: 'clicks',
  status: 'active'
}
```

`fyn.links.delete(id)` resolves with no return value when successful.

`fyn.links.list(input)` parameters:

| 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. |

`LinkListFilter` fields:

| Field | Type | Description |
| --- | --- | --- |
| `domain` | `string` | Filter by domain. |
| `tracking` | `AnalyticsMode` | Filter by analytics mode. |
| `safeMode` | `boolean` | Filter by safe mode setting. |
| `isPrivate` | `boolean` | Filter by privacy setting. |
| `password` | `boolean` | Filter by password protection status. |
| `status` | `LinkStatus` | Filter by link status. |

`viaApi` is response metadata on `LinkRecord` and is set by the backend. It is not a request/filter input.

Available link statuses:

- `LinkStatus.Active`
- `LinkStatus.Inactive`
- `LinkStatus.Flagged`
- `LinkStatus.Reported`
- `LinkStatus.ReportedBanned`
- `LinkStatus.NeedsUpgrade`
- `LinkStatus.Quarantined`

`fyn.links.update(input)` parameters:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `string` | Yes | Link ID to update. |
| `target` | `string` | No | New default destination URL. |
| `title` | `string` | No | Updated title. |
| `notes` | `string` | No | Updated notes. |
| `tags` | `string[]` | No | Updated tags. |
| `iosTarget` | `string` | No | Updated iOS destination URL. |
| `androidTarget` | `string` | No | Updated Android destination URL. |
| `geoTargets` | `Record<string, string>` | No | Updated country-specific targets. |
| `password` | `string \| null` | No | New password, or `null` to remove it. |
| `safeMode` | `boolean` | No | Updated safe mode setting. |
| `privateLink` | `boolean` | No | Updated privacy setting. |
| `analytics` | `AnalyticsMode` | No | Updated analytics mode. |
| `expirySeconds` | `number \| null` | No | Updated expiry, or `null` to clear it. |

## Analytics

Available methods:

- `fyn.analytics.clicks(linkId)`
- `fyn.analytics.timeseries(linkId, query?)`
- `fyn.analytics.browsers(linkId, query?)`
- `fyn.analytics.devices(linkId, query?)`
- `fyn.analytics.countries(linkId, query?)`
- `fyn.analytics.referrers(linkId, query?)`

`MetricsQuery` parameters:

| 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. |

Example:

```ts
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']
});
```

Possible SDK responses:

```ts
// fyn.analytics.clicks('link_123')
{
  id: 'link_123',
  clicks: 120
}

// fyn.analytics.timeseries('link_123', query)
{
  data: [
    {
      date: '2026-03-01T00:00:00+00:00',
      clicks: 14,
      uniqueClicks: 12
    }
  ],
  meta: {
    totalClicks: 120,
    totalUniqueClicks: 95,
    count: 10
  }
}

// fyn.analytics.browsers('link_123', query)
{
  data: [
    { browser: 'Chrome', clicks: 58, percentage: 48.33 },
    { browser: 'Safari', clicks: 29, percentage: 24.17 }
  ],
  meta: {
    totalClicks: 120,
    count: 2
  }
}

// fyn.analytics.devices('link_123', query)
{
  data: [
    { device: 'desktop', clicks: 73, percentage: 60.83 },
    { device: 'mobile', clicks: 47, percentage: 39.17 }
  ],
  meta: {
    totalClicks: 120,
    count: 2
  }
}

// fyn.analytics.countries('link_123', query)
{
  data: [
    { country: 'United States', countryCode: 'US', clicks: 64, percentage: 53.33 },
    { country: 'India', countryCode: 'IN', clicks: 28, percentage: 23.33 }
  ],
  meta: {
    totalClicks: 120,
    count: 2,
    countriesCount: 2
  }
}

// fyn.analytics.referrers('link_123', query)
{
  data: [
    { referrer: 'google.com', clicks: 66, percentage: 55.0 },
    { referrer: 'direct', clicks: 22, percentage: 18.33 }
  ],
  meta: {
    totalClicks: 120,
    count: 2,
    sourcesCount: 2
  }
}
```

## Domains

```ts
const domains = await fyn.domains.list({
  page: 1,
  limit: 10
});

const domain = await fyn.domains.get('domain_123');

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
});

const verification = await fyn.domains.verify(created.id);

const updated = await fyn.domains.update({
  id: created.id,
  description: 'Updated description',
  notFoundUrl: 'https://example.com/not-found',
  rootRedirect: 'https://example.com/home',
  gpc: true
});

await fyn.domains.remove(created.id);
```

Possible SDK responses:

```ts
// fyn.domains.list(...)
{
  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
  }
}

// fyn.domains.get('domain_123')
{
  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'
}

// fyn.domains.create(...)
{
  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'
}

// fyn.domains.verify('domain_123')
{
  id: 'domain_123',
  domain: 'go.example.com',
  status: 'active',
  verified: true,
  cname: 'cname.fyn.link'
}

// fyn.domains.update(...)
{
  id: 'domain_123',
  domain: 'go.example.com',
  description: 'Updated description',
  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'
}
```

`fyn.domains.remove(id)` resolves with no return value when successful.

`fyn.domains.list(input)` parameters:

| Field | Type | Description |
| --- | --- | --- |
| `page` | `number` | Page number. |
| `limit` | `number` | Page size. |

`fyn.domains.create(input)` parameters:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `domain` | `string` | Yes | 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. |

When you create a domain, the backend automatically associates it with the current team from the token used for the request. You do not pass a team in `fyn.domains.create()`.

`fyn.domains.update(input)` parameters:

| 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. |

Domain team ownership is not changed through `fyn.domains.update()`.

## Members

```ts
const members = await fyn.members.list({
  includeOwner: true
});

const member = await fyn.members.get('member_123');

await fyn.members.invite({
  email: 'member@example.com',
  role: 'editor'
});

const updated = await fyn.members.update({
  id: 'member_123',
  role: 'admin'
});

await fyn.members.remove('member_123');
```

Possible SDK responses:

```ts
// fyn.members.list(...)
{
  data: [
    {
      id: 'user_123',
      name: 'Alex Doe',
      email: 'alex@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
  }
}

// fyn.members.get('member_123')
{
  id: 'user_123',
  name: 'Alex Doe',
  email: 'alex@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'
}

// fyn.members.update(...)
{
  id: 'user_123',
  name: 'Alex Doe',
  email: 'alex@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.members.invite(...)` and `fyn.members.remove(...)` both resolve with no return value on success.

`fyn.members.list(input)` parameters:

| Field | Type | Description |
| --- | --- | --- |
| `includeOwner` | `boolean` | Includes the team owner in the result set. |

`fyn.members.invite(input)` parameters:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | `string` | Yes | Member email address. |
| `role` | `string` | Yes | Member role. |

For security, only already-registered users can be invited.

`fyn.members.update(input)` parameters:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `string` | Yes | Member ID to update. |
| `role` | `string` | Yes | Updated role. |

## Teams

```ts
const team = await fyn.teams.get();

const renamed = await fyn.teams.update({
  name: 'Growth Team'
});
```

Possible SDK response (`fyn.teams.get` and `fyn.teams.update`):

```ts
{
  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'
}
```

`fyn.teams.update(input)` parameters:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | `string` | Yes | New team name. |

## Info

```ts
const info = await fyn.info.get();
const freshInfo = await fyn.info.get(true);
```

Use `fyn.info.get()` to fetch bootstrap information for the current token and team. Pass `true` to force a refresh.

Possible SDK response:

```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'
}
```

## Caching

Caching is enabled by default with ETag revalidation support for GET requests.

```ts
const fyn = new Fyn({
  token: process.env.FYN_API_TOKEN!,
  secret: process.env.FYN_SECRET_KEY,
  cache: {
    enabled: true,
    ttlMs: 60_000,
    storage: 'disk',
    path: '.fynlink-sdk-cache.json',
    warnOnDisk: true
  }
});
```

Security notes:

- Disk cache stores raw GET response bodies and headers.
- Link-sensitive fields (for example `target`, `password`, `notes`, `tags`, short URL envelope) remain encrypted inside cached payload envelopes.
- Metadata and headers are cached as returned; they are not encrypted by the SDK cache layer.
- If an attacker obtains both cache contents and your `FYN_SECRET_KEY`, encrypted link payload fields may be decrypted.
- Keep `cache.warnOnDisk` enabled and use disk cache only in trusted environments.

## Error Handling

The SDK throws typed errors:

- `FynAuthError`
- `FynPermissionError`
- `FynNotFoundError`
- `FynValidationError`
- `FynRateLimitError`
- `FynTimeoutError`
- `FynError`

```ts
import { FynValidationError } from '@fynlink/sdk';

try {
  await fyn.links.create({ target: 'https://example.com' });
} catch (error) {
  if (error instanceof FynValidationError) {
    console.error(error.message);
  }
}
```

## Local Examples

```bash
cp examples/.env.example examples/.env
```

Example environment variables:

```bash
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=true
```

Available scripts:

```bash
npx tsx examples/links/create.ts
npx tsx examples/links/get.ts <linkId>
npx tsx examples/links/list.ts
npx tsx examples/links/update.ts <linkId>
npx tsx examples/links/delete.ts <linkId>
npx tsx examples/info/get.ts
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/members/list.ts
npx tsx examples/members/get.ts <memberId>
npx tsx examples/members/invite.ts <email> <role>
npx tsx examples/members/update.ts <memberId> <role>
npx tsx examples/members/remove.ts <memberId>
npx tsx examples/teams/get.ts
npx tsx examples/teams/update.ts <new-team-name>
npx tsx examples/domains/list.ts
npx tsx examples/domains/get.ts <domainId>
npx tsx examples/domains/create.ts <domain>
npx tsx examples/domains/update.ts <domainId>
npx tsx examples/domains/verify.ts <domainId>
npx tsx examples/domains/delete.ts <domainId>
```

More examples: [examples/README.md](/Users/nbk/Algonet/fyn-ts/examples/README.md)

## Development

```bash
npm install
npm run typecheck
npm test
npm run build
```

## License

MIT