npm package: @proveanything/smartlinks
Description: Official JavaScript/TypeScript SDK for the Smartlinks API
Author: Glenn Shoosmith
Latest version: 1.9.14

# @proveanything/smartlinks

Official JavaScript/TypeScript SDK for the Smartlinks API.

Build Smartlinks-powered apps in Node.js or the browser: list collections and products, authenticate users, manage assets and attestations, and call admin endpoints with a clean, typed API.

• TypeScript-first types and intellisense
• Works in Node.js and modern browsers
• Simple auth helpers (login, verify token, request admin/public JWTs)
• Rich resources: collections, products, proofs, assets, attestations, batches, variants, AI, and more
• Optional iframe proxy mode for embedded apps

## Start here

If you're new to the SDK, this is the easiest path:

1. Initialize the SDK with your SmartLinks API base URL.
2. Pick the guide that matches what you're building.
3. Copy one of the working examples and adapt it to your collection/product IDs.

### Pick a guide

- [docs/ai.md](docs/ai.md) — AI responses, chat, RAG, voice, streaming, and product assistants
- [docs/analytics.md](docs/analytics.md) — fire-and-forget web analytics, tag scan telemetry, and dashboard queries
- [docs/translations.md](docs/translations.md) — runtime translation lookup, browser-side caching, and translation admin flows
- [docs/widgets.md](docs/widgets.md) — embeddable React components
- [docs/realtime.md](docs/realtime.md) — subscriptions and live updates
- [docs/iframe-responder.md](docs/iframe-responder.md) — iframe integration and parent/child messaging
- [docs/utils.md](docs/utils.md) — helper utilities and portal path generation

### Most common use cases

- **Build an AI assistant** → start with [docs/ai.md](docs/ai.md)
- **Track page views, clicks, or tag scans** → start with [docs/analytics.md](docs/analytics.md)
- **Translate dynamic content with local browser caching** → start with [docs/translations.md](docs/translations.md)
- **Fetch collections/products** → see [Quick start](README.md#quick-start)
- **Authenticate admins or end users** → see [Authentication](README.md#authentication)
- **Upload and manage files** → see [Assets](README.md#assets)
- **Browse the full surface area** → use [API_SUMMARY.md](API_SUMMARY.md) as reference

For the full list of functions and types, see the API summary:
→ [API Summary](API_SUMMARY.md)

**Documentation:**
- [AI & Chat Completions](docs/ai.md) - Chat completions, RAG, voice integration
- [Analytics](docs/analytics.md) - Fire-and-forget analytics tracking, tag scans, and dashboard queries
- [Translations](docs/translations.md) - Runtime translation lookup, browser-side IndexedDB caching, and admin translation management
- [Widgets](docs/widgets.md) - Embeddable React components
- [Realtime](docs/realtime.md) - Realtime data updates
- [iframe Responder](docs/iframe-responder.md) - iframe integration
- [Utilities](docs/utils.md) - Helper functions for paths, URLs, and common tasks

## Install

```bash
npm install @proveanything/smartlinks
```

## Quick start

Initialize once at app startup with your API base URL. Trailing slashes are optional (normalized). In Node, you can also provide an API key for server-to-server calls. You can enable an ngrok header or supply custom headers.

```ts
import { initializeApi } from '@proveanything/smartlinks'

initializeApi({
  baseURL: 'https://smartlinks.app/api/v1',            // or 'https://smartlinks.app/api/v1/'
  // logger: console,                                   // optional: verbose logging of requests/responses and proxy messages
  // apiKey: process.env.SMARTLINKS_API_KEY,            // Node/server only (optional)
  // ngrokSkipBrowserWarning: true,                    // adds 'ngrok-skip-browser-warning: true'
  // extraHeaders: { 'X-Debug': '1' }                  // merged into every request
})
```

List public collections and fetch products:

```ts
import { collection, product } from '@proveanything/smartlinks'

const collections = await collection.list(false) // public endpoint
const first = collections[0]
if (first) {
  const products = await product.list(first.id, false) // public endpoint
  console.log('First product:', products[0])
}
```

### Quick start by scenario

**Public storefront or portal experience**

```ts
import { initializeApi, collection, product } from '@proveanything/smartlinks'

initializeApi({ baseURL: 'https://smartlinks.app/api/v1' })

const collections = await collection.list(false)
const products = await product.list('collectionId', false)
```

**Server-side admin scripts or back office tools**

```ts
import { initializeApi, product } from '@proveanything/smartlinks'

initializeApi({
  baseURL: 'https://smartlinks.app/api/v1',
  apiKey: process.env.SMARTLINKS_API_KEY,
})

const created = await product.create('collectionId', { name: 'New product' })
console.log(created.id)
```

**AI-powered assistant or setup flow**

```ts
import { initializeApi, ai } from '@proveanything/smartlinks'

initializeApi({
  baseURL: 'https://smartlinks.app/api/v1',
  apiKey: process.env.SMARTLINKS_API_KEY,
})

const response = await ai.chat.responses.create('collectionId', {
  input: 'Write a concise onboarding checklist for a new product launch.'
})

console.log(response.output_text)
```

## Authentication

Use the built-in helpers to log in and verify tokens. After a successful login, the SDK stores the bearer token for subsequent calls.

```ts
import { initializeApi } from '@proveanything/smartlinks'
import { auth } from '@proveanything/smartlinks'

initializeApi({ baseURL: 'https://smartlinks.app/api/v1/' }) // trailing slash OK

// Email + password login (browser or Node)
const user = await auth.login('user@example.com', 'password')
console.log('Hello,', user.name)

// Verify an existing token (and set it if valid)
const verified = await auth.verifyToken(user.bearerToken)
console.log('Token valid?', verified.valid)

// Later, clear token
auth.logout()
```

Admin flows:

```ts
// Request an admin JWT for a collection (requires prior auth)
const jwt = await auth.requestAdminJWT('collectionId')

// Or request a public JWT authorized for a collection/product/proof
const publicJwt = await auth.requestPublicJWT('collectionId', 'productId', 'proofId')
```

## Error Handling

The SDK throws `SmartlinksApiError` for all API errors, providing structured access to:
- **HTTP status code** (`statusCode` / `code`) - Numeric HTTP status (400, 401, 500, etc.)
- **Server error code** (`errorCode`) - String identifier ("NOT_AUTHORIZED", "broadcasts.topic.invalid", etc.)
- **Error message** (`message`) - Human-readable description
- **Additional details** (`details`) - Server-specific fields

### Automatic Error Normalization

The SDK automatically normalizes various server error response formats into a consistent structure:

```ts
// Server may return errors in different formats:
// { errorText: "...", errorCode: "..." }
// { error: "...", message: "..." }
// { ok: false, error: "..." }
// { error: "..." }

// All are normalized to SmartlinksApiError with consistent access:
import { SmartlinksApiError, product } from '@proveanything/smartlinks'

try {
  const item = await product.get('collectionId', 'productId', false)
} catch (error) {
  if (error instanceof SmartlinksApiError) {
    console.error({
      message: error.message,       // "Error 404: Product not found"
      statusCode: error.statusCode, // 404 (HTTP status)
      code: error.code,             // 404 (same as statusCode)
      errorCode: error.errorCode,   // "NOT_FOUND" (server error code string)
      details: error.details,       // Additional server details
      url: error.url,               // Failed URL
    })
    
    // Handle specific server error codes (primary identifier)
    switch (error.errorCode) {
      case 'NOT_AUTHORIZED':
        // Invalid credentials
        break
      case 'broadcasts.topic.invalid':
        // Invalid broadcast topic
        break
      default:
        // Fall back to HTTP status code
        if (error.isNotFound()) {
          // Handle 404
        } else if (error.isAuthError()) {
          // Handle 401/403 - redirect to login
        }
    }
    
    // Use helper methods for HTTP status-based handling
    if (error.isRateLimited()) {
      // Handle 429 - implement retry logic
    } else if (error.isServerError()) {
      // Handle 5xx - show maintenance message
    }
  }
}
```

### Helper Methods

- `error.isClientError()` - 4xx status codes
- `error.isServerError()` - 5xx status codes  
- `error.isAuthError()` - 401 or 403
- `error.isNotFound()` - 404
- `error.isRateLimited()` - 429
- `error.toJSON()` - Serializable object for logging

For comprehensive error handling examples and migration guidance, see [examples/error-handling-demo.ts](examples/error-handling-demo.ts).

## Utility Functions

The SDK includes a simple utility for building portal URLs. Pass in objects and it extracts what it needs:

### Build Portal Paths

```ts
import { utils } from '@proveanything/smartlinks'

// Pass in objects - extracts shortId, portalUrl, product ID, GTIN, etc.
const url = utils.buildPortalPath({
  collection: myCollection,
  product: myProduct  // reads ownGtin from product
})
// Returns: https://portal.smartlinks.io/c/abc123/prod1

// With proof object
const withProof = utils.buildPortalPath({
  collection: myCollection,
  product: myProduct,
  proof: myProof
})
// Returns: https://portal.smartlinks.io/c/abc123/prod1/proof1

// GTIN path (ownGtin read from product.ownGtin)
const gtinPath = utils.buildPortalPath({
  collection: myCollection,
  product: myProduct,  // if product.ownGtin is true, uses /01/ format
  batch: myBatch,      // extracts batch ID and expiry date
  variant: myVariant
})
// Returns: https://portal.smartlinks.io/01/1234567890123/10/batch1/22/var1?17=260630

// Or pass just IDs if you don't have full objects
const simple = utils.buildPortalPath({
  collection: { shortId: 'abc123' },
  productId: 'prod1',
  batchId: 'batch1',  // just string, no expiry
  queryParams: { utm_source: 'email' }
})
// Returns: /c/abc123/prod1?utm_source=email
```

Pass objects where you have them (collection, product, batch, variant, proof) and the function extracts the needed properties. Or pass just IDs (productId, batchId) for simpler cases.

For complete documentation, see [docs/utils.md](docs/utils.md).

## Common tasks

### Products

```ts
import { product } from '@proveanything/smartlinks'

// Public fetch
const item = await product.get('collectionId', 'productId', false)

// Admin create/update/delete (requires auth)
await product.create('collectionId', { name: 'New product' })
await product.update('collectionId', 'productId', { description: 'Updated' })
await product.remove('collectionId', 'productId')
```

### Assets

Upload, list, get, and remove assets within a scope (collection/product/proof).

#### Upload (new)

```ts
import { asset } from '@proveanything/smartlinks'

const uploaded = await asset.upload({
  file, // File from an <input type="file">
  scope: { type: 'proof', collectionId, productId, proofId },
  name: 'hero.png',
  metadata: { description: 'Uploaded via SDK' },
  onProgress: (p) => console.log(`Upload: ${p}%`),
  // appId: 'microapp-123' // optional
})
console.log('Uploaded asset:', uploaded)
```

Deprecated upload helper (wraps to the new `upload` internally):

```ts
// @deprecated Use asset.upload(options)
const legacy = await asset.uploadAsset(collectionId, productId, proofId, file)
```

#### List

```ts
// Collection assets, first 20 images
const list1 = await asset.list({
  scope: { type: 'collection', collectionId },
  mimeTypePrefix: 'image/',
  limit: 20,
})

// Product assets, filter by appId
const list2 = await asset.list({
  scope: { type: 'product', collectionId, productId },
  appId: 'microapp-123',
})
```

#### Get (scoped)

```ts
const a = await asset.get({
  assetId,
  scope: { type: 'proof', collectionId, productId, proofId },
})
```

#### Remove (scoped, admin)

```ts
await asset.remove({
  assetId,
  scope: { type: 'product', collectionId, productId },
})
```

#### Asset response example

```json
{
  "name": "Screenshot 2025-09-15 at 15.21.14",
  "assetType": "Image",
  "type": "png",
  "collectionId": "ChaseAtlantic",
  "url": "https://cdn.smartlinks.app/sites%2FChaseAtlantic%2Fimages%2F2025%2F9%2FScreenshot%202025-09-15%20at%2015%2C21%2C14-1757946214537.png",
  "createdAt": "2005-10-10T23:15:03",
  "hash": "fb98140a6b41ee69b824f29cc8b6795444246f871e4ab2379528b34a4d16284e",
  "thumbnails": {
    "x100": "https://cdn.smartlinks.app/..._100x100.png",
    "x200": "https://cdn.smartlinks.app/..._200x200.png",
    "x512": "https://cdn.smartlinks.app/..._512x512.png"
  },
  "id": "7k1cGErrlmQ94J8yDlVj",
  "site": "ChaseAtlantic",
  "cleanName": "Screenshot 2025-09-15 at 15.21"
}
```

#### Proxy mode and uploads

When `initializeApi({ proxyMode: true })` is active (e.g., SDK runs inside an iframe and the parent performs API calls), the SDK serializes `FormData` for upload into a proxy-safe shape and posts a message to the parent window. The parent proxy handler should detect this payload and reconstruct a native `FormData` before performing the actual HTTP request.

- SDK proxy message body shape for uploads:
  - `{ _isFormData: true, entries: Array<[name: string, value: string | File]> }`
  - Each entry’s value is either a string or a `File` object (structured-cloneable).
- Direct XHR uploads are only used when NOT in proxy mode. In proxy mode, the SDK uses the proxy channel and `postMessage`.

Progress support in proxy mode:

- If you pass `onProgress`, the SDK uses an enhanced upload protocol with chunked messages and progress events.
- Unified envelope protocol (single message shape):
  - Iframe → Parent
    - `{ _smartlinksProxyUpload: true, phase: 'start', id, path, method: 'POST', headers, fields, fileInfo }`
    - `{ _smartlinksProxyUpload: true, phase: 'chunk', id, seq, chunk: ArrayBuffer }`
    - `{ _smartlinksProxyUpload: true, phase: 'end', id }`
  - Parent → Iframe
    - `{ _smartlinksProxyUpload: true, phase: 'ack', id, seq }` (reply via `event.source.postMessage`)
    - `{ _smartlinksProxyUpload: true, phase: 'progress', id, percent }` (reply via `event.source.postMessage`)
    - `{ _smartlinksProxyUpload: true, phase: 'done', id, ok, data? | error? }` (reply via `event.source.postMessage`)

Example parent handler (buffered, simple):

```html
<script>
  const uploads = new Map();
  window.addEventListener('message', async (e) => {
    const m = e.data;
    if (!m || m._smartlinksProxyUpload !== true) return;
    const target = e.source; // reply to the sender iframe
    const origin = e.origin; // consider validating and passing a strict origin
    switch (m.phase) {
      case 'start': {
        uploads.set(m.id, { chunks: [], fields: m.fields, fileInfo: m.fileInfo, path: m.path, headers: m.headers });
        break;
      }
      case 'chunk': {
        const u = uploads.get(m.id);
        if (!u) break;
        u.chunks.push(new Uint8Array(m.chunk));
        target && target.postMessage({ _smartlinksProxyUpload: true, phase: 'ack', id: m.id, seq: m.seq }, origin);
        break;
      }
      case 'end': {
        const u = uploads.get(m.id);
        if (!u) break;
        const blob = new Blob(u.chunks, { type: u.fileInfo.type || 'application/octet-stream' });
        const fd = new FormData();
        for (const [k, v] of u.fields) fd.append(k, v);
        fd.append(u.fileInfo.key || 'file', blob, u.fileInfo.name || 'upload.bin');

        const xhr = new XMLHttpRequest();
        xhr.open('POST', (window.SMARTLINKS_API_BASEURL || '') + u.path);
        for (const [k, v] of Object.entries(u.headers || {})) xhr.setRequestHeader(k, v);
        xhr.upload.onprogress = (ev) => {
          if (ev.lengthComputable) {
            const pct = Math.round((ev.loaded / ev.total) * 100);
            target && target.postMessage({ _smartlinksProxyUpload: true, phase: 'progress', id: m.id, percent: pct }, origin);
          }
        };
        xhr.onload = () => {
          const ok = xhr.status >= 200 && xhr.status < 300;
          try {
            const data = JSON.parse(xhr.responseText);
            target && target.postMessage({ _smartlinksProxyUpload: true, phase: 'done', id: m.id, ok, data, error: ok ? undefined : data?.message }, origin);
          } catch (err) {
            target && target.postMessage({ _smartlinksProxyUpload: true, phase: 'done', id: m.id, ok: false, error: 'Invalid server response' }, origin);
          }
        };
        xhr.onerror = () => target && target.postMessage({ _smartlinksProxyUpload: true, phase: 'done', id: m.id, ok: false, error: 'Network error' }, origin);
        xhr.send(fd);
        uploads.delete(m.id);
        break;
      }
    }
  });
</script>
```

If you maintain the parent proxy, ensure its handler for `_smartlinksProxyRequest`:
- Detects `body._isFormData === true` and rebuilds `const fd = new FormData(); for (const [k,v] of body.entries) fd.append(k, v);`
- Issues the HTTP request with that `FormData` and returns the parsed JSON response back to the iframe.

### AI helpers

```ts
import { ai } from '@proveanything/smartlinks'

const content = await ai.generateContent('collectionId', {
  contents: 'Write a friendly product blurb for our coffee beans.',
  responseMimeType: 'text/plain',
  provider: 'openai',
  model: 'gpt-4o'
})
console.log(content)
```

### Analytics (Actions & Broadcasts)

Track user actions and broadcast deliveries per collection. These endpoints live under admin and require valid auth (API key or bearer token).

```ts
import { actions, broadcasts } from '@proveanything/smartlinks'

// List action events for a user
const actionEvents = await actions.byUser('collectionId', {
  userId: 'user_123',
  from: '2025-01-01T00:00:00Z',
  to: '2025-12-31T23:59:59Z',
  limit: 100,
})

// Outcome counts, optionally deduping latest per actor
const counts = await actions.countsByOutcome('collectionId', {
  actionId: 'click',
  dedupeLatest: true,
  idField: 'userId',
})

// Append a single action event
await actions.append('collectionId', {
  userId: 'user_123',
  actionId: 'click',
  outcome: 'success',
})

// Broadcast recipients and filters
const recipients = await broadcasts.recipientIds('collectionId', {
  broadcastId: 'br_456',
  idField: 'contactId',
})

// Append broadcast recipients in bulk (preferred shape)
await broadcasts.appendBulk('collectionId', {
  params: { broadcastId: 'br_456', channel: 'email' },
  ids: ['contact_1','contact_2'],
  idField: 'contactId',
})
```

### Broadcast sending and previews

```ts
import { broadcasts } from '@proveanything/smartlinks'

// Preview a broadcast (HTML)
const preview = await broadcasts.preview('collectionId', 'broadcastId', {
  contactId: 'contact_123',
  props: { firstName: 'Sam' },
})

// Send a test email
await broadcasts.sendTest('collectionId', 'broadcastId', {
  to: 'test@example.com',
  subject: 'Test subject',
  props: { foo: 'bar' },
})

// Enqueue broadcast sending (background)
await broadcasts.send('collectionId', 'broadcastId', {
  pageSize: 100,
  sharedContext: { campaign: 'summer' },
})

// Manual page send (for testing/UX)
const manual = await broadcasts.sendManual('collectionId', 'broadcastId', {
  limit: 50,
  dryRun: true,
})
```

## Communications Overview

End-to-end explainer covering comms settings (unsubscribe, types), Web Push registration, and multi-channel broadcasts with SDK examples: [src/docs/smartlinks/comms-explainer.md](src/docs/smartlinks/comms-explainer.md)

### Async jobs

```ts
import { async as slAsync, jobs } from '@proveanything/smartlinks'

// Enqueue an async job
const queued = await slAsync.enqueueAsyncJob('collectionId', {
  task: 'email:daily-digest',
  payload: { segmentId: 'seg_1' },
  priority: 5,
  key: 'digest:seg_1',
})

// Check job status
const status = await slAsync.getAsyncJobStatus('collectionId', queued.id)

// List recent jobs
const recent = await jobs.listJobs('collectionId', { state: 'queued', limit: 20 })

// Get a single job
const job = await jobs.getJob('collectionId', queued.id)
```

## Browser and React

The SDK works in modern browsers. Initialize once and call public endpoints without an API key; authenticate to access protected/admin endpoints.

```ts
import { initializeApi } from '@proveanything/smartlinks'
import { collection } from '@proveanything/smartlinks'

initializeApi({ baseURL: 'https://smartlinks.app/api/v1' })
const collections = await collection.list(false)
```

For a fuller UI example, see `examples/react-demo.tsx`.

## Iframe Integration

When embedding Smartlinks inside an iframe (set `proxyMode: true` in `initializeApi` if you need parent-proxy API calls), you can also send UI/control messages to the parent window. The SDK provides lightweight helpers in `iframe.ts`:

```ts
import { enableAutoIframeResize, disableAutoIframeResize, redirectParent, sendParentCustom, isIframe } from '@proveanything/smartlinks'

// Automatically push height changes to parent so it can resize the iframe.
enableAutoIframeResize({ intervalMs: 150 })

// Later disable if not needed:
disableAutoIframeResize()

// Redirect parent window to a URL (e.g. after login):
redirectParent('https://app.example.com/dashboard')

// Send any custom event + payload:
sendParentCustom('smartlinks:navigate', { url: '/profile' })

if (isIframe()) {
  console.log('Running inside an iframe')
}
```

Parent page can listen for these messages:

```ts
window.addEventListener('message', (e) => {
  const msg = e.data
  if (!msg || !msg._smartlinksIframeMessage) return
  switch (msg.type) {
    case 'smartlinks:resize':
      // adjust iframe height
      const iframeEl = document.getElementById('smartlinks-frame') as HTMLIFrameElement
      if (iframeEl) iframeEl.style.height = msg.payload.height + 'px'
      break
    case 'smartlinks:redirect':
      window.location.href = msg.payload.url
      break
    case 'smartlinks:navigate':
      // Custom example
      console.log('Navigate request:', msg.payload.url)
      break
  }
})
```

Notes:
- Auto-resize uses `ResizeObserver` when available, falling back to `MutationObserver` + polling.
- In non-browser or Node environments these helpers safely no-op.
- Use `sendParentCustom` for any domain-specific integration events.

## Configuration reference

```ts
initializeApi({
  baseURL: string, // with or without trailing slash
  apiKey?: string,      // Node/server only
  bearerToken?: string, // optional at init; set by auth.login/verifyToken
  proxyMode?: boolean   // set true if running inside an iframe and using parent proxy
  ngrokSkipBrowserWarning?: boolean // forces header 'ngrok-skip-browser-warning: true'
  extraHeaders?: Record<string,string> // custom headers merged in each request
  iframeAutoResize?: boolean // default true when embedded in an iframe
  logger?: Function | { debug?: Function; info?: Function; warn?: Function; error?: Function; log?: Function } // optional verbose logging
})

// Auto-detection: If baseURL contains '.ngrok.io' or '.ngrok-free.dev' the header is added automatically
// unless you explicitly set ngrokSkipBrowserWarning: false.
// Auto iframe resize: When in an iframe, resize messages are sent by default unless iframeAutoResize: false.
// Verbose logging: Pass a logger (e.g. console) to log outbound requests/responses and proxy postMessages.
```

When embedding the SDK in an iframe with `proxyMode: true`, you can also use:

```ts
import { sendCustomProxyMessage } from '@proveanything/smartlinks'
const data = await sendCustomProxyMessage('my-action', { foo: 'bar' })

// Toggle ngrok header or update custom headers later:
import { setNgrokSkipBrowserWarning, setExtraHeaders } from '@proveanything/smartlinks'
setNgrokSkipBrowserWarning(true)
setExtraHeaders({ 'X-Debug': '1' })
```

## Full API surface

Explore every function, parameter, and type here:

- [API_SUMMARY.md](docs/API_SUMMARY.md) - Complete API reference

## Additional Documentation

The SDK includes comprehensive guides for advanced features:

- **[Liquid Templates](docs/liquid-templates.md)** - Dynamic templating for emails and notifications with personalized data
- **[Real-Time Messaging](docs/realtime.md)** - Ably integration for live chat, presence, and real-time updates
- **[Theme System](docs/theme.system.md)** - Dynamic theming for iframe apps with CSS variables and postMessage
- **[Theme Defaults](docs/theme-defaults.md)** - Default theme configuration reference
- **[Widgets](docs/widgets.md)** - React widget system for embeddable components
- **[Internationalization](docs/i18n.md)** - Multi-language support and localization

## Requirements

- Node.js 16+ or modern browsers
- TypeScript 4.9+ (if using TS)

## License

MIT © Prove Anything