@boldvideo/bold-js
v1.26.0
Published
<div align="center"> <a href="https://boldvideo.com?utm_source=github.com&utm_medium=readme&utm_campaign=bold-js" align="center"> <img src="https://boldvideo.com/bold-js-github-header.svg" alt="Bold Logo"> </a> <h1 align="center rainbow">@boldvid
Readme
Installation
npm install @boldvideo/bold-jsQuick Start
import { createClient } from '@boldvideo/bold-js';
const bold = createClient('your-api-key');
// Fetch videos
const videos = await bold.videos.list();
// AI-powered recommendations
const recs = await bold.ai.recommendations({
topics: ['sales', 'negotiation'],
stream: false
});
console.log(recs.guidance);API Reference
Videos
// List latest videos (default: 12)
const videos = await bold.videos.list();
// With limit (backwards compatible)
const videos = await bold.videos.list(20);
// With filters
const videos = await bold.videos.list({
limit: 20,
tag: 'sales',
collectionId: 'col_123',
viewerId: 'viewer_123' // Include watch progress
});
// Paginated index (uses /videos endpoint)
const videos = await bold.videos.list({
page: 2,
tag: 'sales',
collectionId: 'col_123'
});
// Get a single video by ID or slug
const video = await bold.videos.get('video-id');
const videoBySlug = await bold.videos.get('my-video-slug');
// Search videos
const results = await bold.videos.search('pricing strategies');Playlists
// List all playlists
const playlists = await bold.playlists.list();
// Get a single playlist with videos
const playlist = await bold.playlists.get('playlist-id');Settings
// Fetch channel settings, menus, and featured playlists
const settings = await bold.settings();
// Access portal hero configuration
if (settings.portal.hero.type === 'custom') {
// Render custom hero section
}
// Menu items with external link handling
settings.menuItems.forEach(item => {
// item.blank: true opens in a new window/tab
// item.isExt: true indicates an external URL
// item.icon: optional icon path (can be null)
});Viewers API
Manage external users and track their video watch progress. Ideal for course platforms integrating with Bold Video.
Viewer Management
// Create a viewer (e.g., when user signs up)
const { data: viewer } = await bold.viewers.create({
name: 'John Doe',
externalId: 'user_123', // Your platform's user ID
email: '[email protected]',
traits: { plan: 'pro', company_name: 'Acme Inc' }
});
// Find viewer by external ID (common for syncing users)
const { data: viewer } = await bold.viewers.lookup({ externalId: 'user_123' });
// Or find by email
const { data: viewer } = await bold.viewers.lookup({ email: '[email protected]' });
// Update viewer
await bold.viewers.update(viewer.id, {
traits: { plan: 'enterprise' } // Note: traits are replaced, not merged
});
// List all viewers
const { data: viewers } = await bold.viewers.list();Progress Tracking
// Save progress as video plays (call every 5-10 seconds)
await bold.viewers.saveProgress(viewerId, videoId, {
currentTime: 120, // seconds
duration: 600 // total video duration
});
// Mark video complete by setting currentTime = duration
await bold.viewers.saveProgress(viewerId, videoId, {
currentTime: 600,
duration: 600
});
// Get progress for a specific video
const { data: progress } = await bold.viewers.getProgress(viewerId, videoId);
console.log(`${progress.percentage}% complete`);
// List all progress for a viewer (e.g., for a course dashboard)
const { data: progress, meta } = await bold.viewers.listProgress(viewerId, {
collectionId: 'course-collection-id', // Filter to a course
completed: false // Only in-progress videos
});
console.log(`Completed ${meta.completed} of ${meta.total} videos`);Notification Preferences
Read or partially update a viewer's email and push notification channels with the tenant API-key client:
const preferences = await bold.notifications.getPreferences('viewer-uuid');
console.log(preferences.channels.email, preferences.channels.push);
// Only push is changed; the existing email preference is preserved.
await bold.notifications.updatePreferences('viewer-uuid', {
channels: { push: true }
});Session Management
Use BOLD Session Management as a headless password-sharing prevention layer. Your app still owns login, membership, product access, and staff/admin checks. Call BOLD only after your app has decided the viewer may access protected content.
Runtime Sessions
Runtime session calls use the customer's upstream JWT, not a BOLD API key. This factory is safe for browser/native runtime code when the upstream JWT is already available to that runtime.
import { createAuthClient } from '@boldvideo/bold-js';
const auth = createAuthClient({
tenantSlug: 'hrtu',
upstreamJwt: outsetaJwt
});
const result = await auth.sessions.create({
deviceId: browserDeviceId,
platform: 'web',
userAgent: navigator.userAgent
});
let sessionId: string | undefined;
if ('challengeRequired' in result) {
// Show an email-code challenge UI, then verify the code.
const verified = await auth.challenges.verify(result.challengeId, '123456');
sessionId = verified.sessionId ?? undefined;
} else if (result.sessionId) {
sessionId = result.sessionId;
}
if (sessionId) {
localStorage.setItem('bold_session_id', sessionId);
}
if (sessionId) {
// Register the session for push notifications.
await auth.notifications.registerDevice(sessionId, {
provider: 'expo', // 'expo' | 'fcm' | 'apns'
token: expoPushToken
});
const verification = await auth.sessions.verify(sessionId);
if (!verification.valid) {
// session_not_found, session_revoked, or session_expired
}
}For refreshed JWTs, pass an override per call:
await auth.sessions.verify(sessionId, { upstreamJwt: freshJwt });Viewer self-management methods require policy support:
const { data: sessions } = await auth.sessions.list();
await auth.sessions.revoke(sessionId);
await auth.sessions.revokeOthers(currentSessionId);When Session Management is not available for the account, device registration returns a typed error:
import { SessionManagementUnavailableError } from '@boldvideo/bold-js';
try {
await auth.notifications.registerDevice(sessionId, {
provider: 'fcm',
token: fcmToken
});
} catch (error) {
if (error instanceof SessionManagementUnavailableError) {
console.log(error.status, error.code, error.retryable);
}
}Server-Side Session Management
Customer servers can inspect BOLD device state, revoke sessions, and manage per-viewer device-limit overrides or exemptions by upstream external ID using the normal BOLD tenant API key. Keep this API key server-side. Do not put a BOLD tenant key or admin key in browser code.
import { createClient } from '@boldvideo/bold-js';
const bold = createClient(process.env.BOLD_TENANT_API_KEY!);
const { data: state } =
await bold.sessionManagement.getViewerSessionManagementStateByExternalId(
outsetaPersonUid
);
if (state.canUpdateDeviceLimitOverride) {
await bold.sessionManagement.setViewerDeviceLimitOverrideByExternalId(
outsetaPersonUid,
5
);
await bold.sessionManagement.clearViewerDeviceLimitOverrideByExternalId(
outsetaPersonUid
);
}
if (state.canUpdateSessionManagementExemption) {
await bold.sessionManagement.setViewerSessionManagementExemptionByExternalId(
outsetaPersonUid,
true
);
}
const { data: sessions } =
await bold.sessionManagement.listViewerSessionsByExternalId(outsetaPersonUid);
for (const session of sessions) {
if (session.travelVerdict === 'impossible_travel') {
// Bold flagged this sign-in as physically implausible.
}
}
await bold.sessionManagement.revokeAllViewerSessionsByExternalId(
outsetaPersonUid,
{ reason: 'outseta:membership_changed' }
);setViewerDeviceLimitOverrideByExternalId requires a positive safe integer.
Use clearViewerDeviceLimitOverrideByExternalId for explicit clearing.
Each session item carries travelVerdict: 'impossible_travel' | null — Bold's
authoritative impossible-travel signal, exposed by these admin-scoped
session-management methods. It is a label only: the SDK never returns
coordinates, the raw IP, or derived distance/speed. It is null when the
sign-in was not flagged (and for backends that predate the field).
The server-side session-management methods cannot update JWT config, account feature flags, session TTL, or email template setup. Those remain BOLD operator controls.
Community API
Build community features with posts, comments, and reactions. All write operations require a viewerId (the viewer performing the action).
Posts
// List posts (optionally filter by category)
const { data: posts } = await bold.community.posts.list({
category: 'announcements',
limit: 20,
offset: 0,
viewerId: 'viewer-uuid' // Include viewerReacted in response
});
// Get a single post with comments
const { data: post } = await bold.community.posts.get('post-id', 'viewer-uuid');
// Create a post and notify viewers by their customer external IDs.
const { data: newPost, mentions } = await bold.community.posts.create('viewer-uuid', {
content: 'Hello community! **Markdown** supported.',
category: 'general',
mentions: ['customer-user-123']
});
console.log(mentions?.skipped); // malformed or unresolved external IDs
// Update a post (owner or admin only)
await bold.community.posts.update('viewer-uuid', 'post-id', {
content: 'Updated content'
});
// Delete a post (owner or admin only)
await bold.community.posts.delete('viewer-uuid', 'post-id');
// React to a post (toggle like/unlike)
const reaction = await bold.community.posts.react('viewer-uuid', 'post-id');
console.log(reaction.reacted, reaction.reactionsCount);Comments
// Create a comment on a post
const { data: comment } = await bold.community.comments.create(
'viewer-uuid',
'post-id',
{ content: 'Great post!', mentions: ['customer-user-456'] }
);
// Reply to a comment (nested)
const { data: reply } = await bold.community.comments.create(
'viewer-uuid',
'post-id',
{ content: 'I agree!', parentId: 'parent-comment-id' }
);
// Delete a comment (owner or admin only)
await bold.community.comments.delete('viewer-uuid', 'comment-id');
// React to a comment (toggle)
const reaction = await bold.community.comments.react('viewer-uuid', 'comment-id');
// Read nested reaction state on comments/replies (viewerId enables viewerHasReacted)
const { data: post } = await bold.community.posts.get('post-id', 'viewer-uuid');
for (const thread of post.comments.items ?? []) {
console.log(thread.createdAt, thread.reactions.count, thread.reactions.viewerHasReacted);
}Mention Inbox
const { data: mentionItems, meta } = await bold.community.mentions.list(
'viewer-uuid',
{ page: 1, pageSize: 20 }
);
const { data: unread } =
await bold.community.mentions.unreadCount('viewer-uuid');
await bold.community.mentions.markRead('viewer-uuid', {
ids: mentionItems.map((mention) => mention.id)
});
// Or mark every unread mention as read.
await bold.community.mentions.markRead('viewer-uuid', { all: true });The optional mentions result is returned on post/comment creation only when
the request included a mentions array. Bold processes at most 25 supplied
external IDs; malformed, unresolved, or beyond-cap IDs appear in skipped,
while duplicates and self-mentions are omitted silently. A skipped ID never
fails content creation.
Mention excerpt values are plain, unsanitized content. Escape or sanitize
them before rendering as HTML.
AI Methods
All AI methods support both streaming (default) and non-streaming modes.
Chat
Library-wide conversational AI for deep Q&A across your entire video library.
// Streaming (default)
const stream = await bold.ai.chat({ prompt: 'How do I price my SaaS?' });
for await (const event of stream) {
if (event.type === 'text_delta') process.stdout.write(event.delta);
if (event.type === 'sources') console.log('Sources:', event.sources);
}
// Non-streaming
const response = await bold.ai.chat({
prompt: 'What are the best closing techniques?',
stream: false
});
console.log(response.content);Options:
| Parameter | Type | Description |
|-----------|------|-------------|
| prompt | string | The user's question (required) |
| stream | boolean | true (default) for SSE, false for JSON |
| videoId | string | If provided, scope to this video instead of whole library |
| currentTime | number | Current playback position (only with videoId) |
| conversationId | string | Pass to continue existing conversation |
| collectionId | string | Filter to a specific collection |
| tags | string[] | Filter by tags |
Recommendations
Get AI-powered video recommendations based on topics — ideal for personalized learning paths, exam prep, and content discovery.
// Streaming (default)
const stream = await bold.ai.recommendations({
topics: ['contract law', 'ethics', 'client management'],
});
for await (const event of stream) {
if (event.type === 'recommendations') {
event.recommendations.forEach(rec => {
console.log(`${rec.topic}:`);
rec.videos.forEach(v => console.log(` - ${v.title} (${v.relevance})`));
});
}
if (event.type === 'text_delta') {
process.stdout.write(event.delta); // AI guidance
}
}
// Non-streaming
const response = await bold.ai.recommendations({
topics: ['sales', 'marketing'],
stream: false
});
console.log(response.guidance);
console.log(response.recommendations);Options:
| Parameter | Type | Description |
|-----------|------|-------------|
| topics | string[] | Topics to find content for (required) |
| stream | boolean | true (default) for SSE, false for JSON |
| limit | number | Max videos per topic (default: 5, max: 20) |
| collectionId | string | Filter to a specific collection |
| tags | string[] | Filter by tags |
| includeGuidance | boolean | Include AI learning path narrative (default: true) |
| context | AIContextMessage[] | Previous conversation turns for follow-ups |
Search
Fast semantic search with a brief AI-generated summary.
const stream = await bold.ai.search({
prompt: 'pricing strategies',
limit: 10
});
for await (const event of stream) {
if (event.type === 'sources') {
console.log(`Found ${event.sources.length} results`);
}
}Video-Scoped Chat
Chat about a specific video by passing videoId. Uses only that video's transcript as context.
const stream = await bold.ai.chat({
videoId: 'video-id',
prompt: 'What is discussed at the 5 minute mark?'
});
for await (const event of stream) {
if (event.type === 'text_delta') process.stdout.write(event.delta);
}
// With playback context (coming soon)
const stream = await bold.ai.chat({
videoId: 'video-id',
prompt: 'What does she mean by that?',
currentTime: 847 // seconds
});Get Conversation History
Retrieve a conversation by ID to display message history:
const conversation = await bold.ai.getConversation('550e8400-e29b-41d4-a716-446655440000');
console.log(`Created: ${conversation.createdAt}`);
for (const msg of conversation.messages) {
console.log(`${msg.role}: ${msg.content}`);
}Multi-turn Conversations
Use the context parameter for follow-up questions:
const first = await bold.ai.search({
prompt: 'How do indie designers find clients?',
stream: false
});
const followUp = await bold.ai.search({
prompt: 'What about cold outreach specifically?',
context: first.context,
stream: false
});Conversation Starters
Fetch conversation starter prompts, resolved per collection when available and falling back to the account-level starters otherwise:
// Account-level starters (no collectionIds)
const { data: starters } = await bold.conversationStarters.list();
// Resolve starters for one or more collections
const { data: starters } = await bold.conversationStarters.list({
collectionIds: ['collection-uuid-1', 'collection-uuid-2']
});
starters.forEach((starter) => {
console.log(starter.text, starter.source, starter.collectionId);
});Analytics
Track video events and page views for analytics.
// Track video events (play, pause, complete, etc.)
bold.trackEvent({
type: 'play',
videoId: 'video-id',
timestamp: 0
});
// Track page views
bold.trackPageView({
path: '/videos/my-video',
referrer: document.referrer
});TypeScript
All types are exported for full TypeScript support:
import type {
Video,
Playlist,
Settings,
Portal,
PortalHero,
MenuItem,
AIEvent,
AIResponse,
ChatOptions,
SearchOptions,
RecommendationsOptions,
RecommendationsResponse,
Recommendation,
Conversation,
ConversationMessage,
Source,
Viewer,
ViewerProgress,
ViewerLookupParams,
ListProgressOptions,
ListVideosOptions,
ListVideosLatestOptions,
ListVideosIndexOptions,
NotificationChannels,
NotificationPreferencesResponse,
NotificationChannelUpdate,
UpdateNotificationPreferencesData,
NotificationProvider,
RegisterDeviceData,
RegisterDeviceResponse,
// Community API
Post,
PostAuthor,
Comment,
ReactionResponse,
Mention,
MentionSkippedReport,
CommunityPostCreateResponse,
CommunityCommentCreateResponse,
ListPostsOptions,
ListMentionsOptions,
MentionsUnreadCountResponse,
MarkMentionsReadData,
MarkMentionsReadResponse,
CreatePostData,
UpdatePostData,
CreateCommentData,
// Conversation Starters
ConversationStarter,
ConversationStarterSource,
ListConversationStartersOptions
} from '@boldvideo/bold-js';Migration from v1.7.x
Breaking: All response types now use camelCase
All API responses (videos, playlists, settings, AI) are now transformed to use idiomatic TypeScript/JavaScript naming:
// Before (v1.7.x and earlier)
video.playback_id
video.published_at
video.stream_url
video.meta_data
settings.featured_playlists
settings.menu_items
settings.theme_config
playlist.is_private
// After (v1.8.0)
video.playbackId
video.publishedAt
video.streamUrl
video.metaData
settings.featuredPlaylists
settings.menuItems
settings.themeConfig
playlist.isPrivateMethod Changes
| Old | New | Notes |
|-----|-----|-------|
| bold.ai.ask(opts) | bold.ai.chat(opts) | ask() still works but is deprecated |
| bold.ai.coach(opts) | bold.ai.chat(opts) | coach() still works but is deprecated |
| bold.ai.chat(videoId, opts) | bold.ai.chat({ videoId, ...opts }) | Pass videoId in options |
| bold.ai.recommend(opts) | bold.ai.recommendations(opts) | recommend() still works but is deprecated |
Type Renames
| Old Type | New Type |
|----------|----------|
| AskOptions | ChatOptions |
| RecommendOptions | RecommendationsOptions |
| RecommendResponse | RecommendationsResponse |
The old types are still exported as aliases for backward compatibility.
Related Links
Contributing
See CONTRIBUTING.md for details on how to contribute to this project.
Security
See SECURITY.md for security policies and reporting vulnerabilities.
License
MIT
