Node.js SDK

@seenn/node — Full TypeScript support with fluent API

npm version GitHub stars MIT License

#Installation

npm install @seenn/node

Or with other package managers:

# yarn
yarn add @seenn/node

# pnpm
pnpm add @seenn/node

#SeennClient

The main client class for interacting with Seenn.

new SeennClient(config: SeennConfig)

Creates a new Seenn client instance.

Config Options
apiKey string Your secret API key (sk_live_xxx or sk_test_xxx)required
baseUrl string API base URL (default: https://api.seenn.io)
timeout number Request timeout in ms (default: 30000)
maxRetries number Max retry attempts (default: 3)
debug boolean Enable debug logging (default: false)
import { SeennClient } from '@seenn/node';

const seenn = new SeennClient({
  apiKey: process.env.SEENN_SECRET_KEY,
  debug: process.env.NODE_ENV === 'development',
});

#seenn.jobs.start()

seenn.jobs.start(params: StartJobParams): Promise<Job>

Start a new job. Returns a Job instance for fluent updates.

Parameters
userId string User ID who owns this jobrequired
jobType string Job type identifier (e.g., 'video-generation')required
title string Human-readable title shown to userrequired
metadata object Custom metadata (max 10KB)
queue QueueInfo Initial queue position info
stage StageInfo Initial stage info
estimatedCompletionAt string ISO 8601 timestamp for estimated completion
ttlSeconds number Time-to-live in seconds (default: 30 days)
workflowId string ETA tracking key (defaults to jobType)
estimatedDuration number Default ETA in milliseconds (1s - 24h)
const job = await seenn.jobs.start({
  userId: 'user_123',
  jobType: 'video-generation',
  title: 'Creating your video...',
  workflowId: 'sora-v2',        // Track ETAs per model
  estimatedDuration: 120000,   // 2 min default
  metadata: {
    prompt: 'A cat playing piano',
    quality: 'high',
  },
  stage: {
    name: 'initializing',
    current: 1,
    total: 3,
  },
});

console.log(job.id);  // '01HXYZ...'
console.log(job.estimatedCompletionAt);  // '2026-01-23T14:32:00Z'

#seenn.jobs.get()

seenn.jobs.get(jobId: string): Promise<Job>

Get a job by ID. Useful for resuming updates in a different process.

// In a worker process
const job = await seenn.jobs.get('job_abc123');

console.log(job.status);    // 'running'
console.log(job.progress);  // 25

#seenn.jobs.list()

seenn.jobs.list(userId: string, options?): Promise<{ jobs: Job[], nextCursor?: string }>

List jobs for a user with pagination.

Options
limit number Max jobs to return (default: 20, max: 100)
cursor string Pagination cursor from previous response
const { jobs, nextCursor } = await seenn.jobs.list('user_123', {
  limit: 10,
});

for (const job of jobs) {
  console.log(job.title, job.status);
}

// Fetch next page
if (nextCursor) {
  const nextPage = await seenn.jobs.list('user_123', { cursor: nextCursor });
}

#Parent-Child Jobs

For batch processing workflows, you can create parent jobs that contain multiple child jobs. The parent's progress is automatically calculated from its children.

Use Cases: Batch image processing (e.g., 5-image packs), multi-stage video pipelines, multi-model AI workflows.

#seenn.jobs.createParent()

seenn.jobs.createParent(params: CreateParentParams): Promise<Job>

Create a parent job that will contain child jobs.

Parameters
userId string User ID who owns this jobrequired
jobType string Job type identifierrequired
title string Human-readable titlerequired
childCount number Total number of children (1-1000)required
childProgressMode 'average' | 'weighted' | 'sequential' How to calculate parent progress (default: 'average')
const parent = await seenn.jobs.createParent({
  userId: 'user_123',
  jobType: 'batch-processing',
  title: 'Christmas Image Pack (5 images)',
  childCount: 5,
  childProgressMode: 'average',
});

console.log(parent.isParent);  // true
console.log(parent.children);   // { total: 5, completed: 0, ... }

#seenn.jobs.createChild()

seenn.jobs.createChild(params: CreateChildParams): Promise<Job>

Create a child job under a parent.

Parameters
parentJobId string Parent job IDrequired
childIndex number 0-based index within parentrequired
userId string User ID (must match parent)required
jobType string Job type identifierrequired
title string Human-readable titlerequired
const child = await seenn.jobs.createChild({
  parentJobId: parent.id,
  childIndex: 0,
  userId: 'user_123',
  jobType: 'image-generation',
  title: 'Snowflake Image',
});

console.log(child.isChild);  // true
console.log(child.parent);   // { parentJobId: '...', childIndex: 0 }

// Update child progress - parent auto-updates!
await child.setProgress(50);
await child.complete();

#seenn.jobs.createBatch()

seenn.jobs.createBatch(params: CreateBatchParams): Promise<{ parent: Job, children: Job[] }>

Create a parent job and all children in one call. Best for batch processing workflows.

Parameters
userId string User ID who owns these jobsrequired
jobType string Job type identifier (same for parent and children)required
parentTitle string Title for the parent jobrequired
childTitles string[] Titles for each child job (array length = child count)required
childProgressMode 'average' | 'sequential' How to calculate parent progress (default: 'average')
metadata object Metadata for parent job

Complete Example: Glow Image Pack

This example shows a complete batch processing workflow for generating a 5-image Christmas pack:

// 1. Create batch job (parent + 5 children)
const { parent, children } = await seenn.jobs.createBatch({
  userId: 'user_123',
  jobType: 'image-pack',
  parentTitle: 'Christmas Pack (5 images)',
  childTitles: [
    'Snowflake',
    'Christmas Tree',
    'Santa Claus',
    'Reindeer',
    'Gift Box',
  ],
  childProgressMode: 'average',
  metadata: { packId: 'christmas-2026', style: 'watercolor' },
});

console.log('Parent ID:', parent.id);          // '01HXY...'
console.log('Children:', children.length);     // 5
console.log('Parent status:', parent.status); // 'pending'

// 2. Process children in parallel with your AI model
await Promise.all(children.map(async (child, index) => {
  try {
    // Update progress as AI processes
    await child.setProgress(10, { message: 'Starting generation...' });

    // Call your AI model
    const imageUrl = await generateImage(child.title, {
      onProgress: (pct) => child.setProgress(pct),
    });

    // Complete with result URL
    await child.complete({
      result: { type: 'image', url: imageUrl },
      message: 'Image ready!',
    });
  } catch (error) {
    // Fail individual child - parent continues!
    await child.fail({
      error: { code: 'GENERATION_FAILED', message: error.message },
      retryable: true,
    });
  }
}));

// 3. Check final results
await parent.refresh();
console.log('Parent status:', parent.status);       // 'completed' or 'failed'
console.log('Completed:', parent.children.completed); // 4
console.log('Failed:', parent.children.failed);       // 1
Auto-updates: When children update, the parent's progress and status automatically recalculate. Your mobile app receives updates for both child and parent changes.

#seenn.jobs.getWithChildren()

seenn.jobs.getWithChildren(parentJobId: string): Promise<{ parent: Job, children: ChildJobSummary[] }>

Get a parent job with all its children.

const { parent, children } = await seenn.jobs.getWithChildren(parentId);

console.log(parent.progress);            // 60 (average of children)
console.log(parent.childProgress);      // { completed: 3, running: 1, pending: 1, ... }

for (const child of children) {
  console.log(`Child ${child.childIndex}: ${child.status} (${child.progress}%)`);
}

#childProgressMode

The childProgressMode option determines how the parent job's progress is calculated from its children:

Mode Calculation Best For
'average' Sum of all child progress / total children Equal-weight tasks (e.g., batch image generation)
'sequential' (Completed children / total) × 100 Pipeline stages where partial progress doesn't matter
'weighted' Same as average (custom weights coming soon) Variable-size tasks (future feature)
// Example: 5 children, 3 at 100%, 1 at 50%, 1 at 0%
// Total progress: 100 + 100 + 100 + 50 + 0 = 350

// 'average' mode: 350 / 5 = 70%
// 'sequential' mode: 3 completed / 5 total = 60%

#Failed Child Behavior

Seenn allows partial success - not every child needs to complete for the parent to succeed.

Parent Status Rules

Scenario Parent Status Example
All children completed completed 5/5 completed, 0 failed
Some children failed, some completed completed 4/5 completed, 1 failed (partial success)
ALL children failed failed 0/5 completed, 5 failed
Still processing running 2 completed, 1 running, 2 pending
Key Rule: Parent fails ONLY when completed === 0 AND all children are in terminal state. If even ONE child completes, the parent completes (with partial results).

Progress Calculation with Failed Children

  • Failed children count toward progress - Their last progress value is used
  • Example: 3 at 100%, 1 at 50% (failed at 50%), 1 at 0% (failed immediately) = (100+100+100+50+0)/5 = 70%
  • Use parent.children.failed to check how many failed

Handling Partial Failures

// After batch processing completes
const { parent, children } = await seenn.jobs.getWithChildren(parentId);

const { completed, failed, total } = parent.children;

if (parent.status === 'completed' && failed > 0) {
  // Partial success - some children failed
  console.log(`Pack completed with ${completed}/${total} images`);

  // Get successful results
  const successfulImages = children
    .filter(c => c.status === 'completed')
    .map(c => c.result?.url);

  // Get failed children for retry
  const failedChildren = children.filter(c => c.status === 'failed');
  for (const child of failedChildren) {
    console.log(`"${child.title}" failed: ${child.error?.message}`);
    // Optionally offer retry to user
  }
} else if (parent.status === 'failed') {
  // Total failure - ALL children failed
  console.log('Pack generation completely failed');
}

#job.setProgress()

job.setProgress(progress: number, options?: ProgressOptions): Promise<Job>

Update job progress. Automatically sets status to running if pending.

Parameters
progress number Progress percentage (0-100)required
message string Status message to show user
stage StageInfo Current stage info { name, current, total, description? }
queue QueueInfo Queue position { position, total?, queueName? }
estimatedCompletionAt string Updated ETA (ISO 8601)
metadata object Additional metadata (merged with existing)
push PushOptions Push notification options (see Push Configuration)
Push Options Location

Push notifications can be sent from setProgress(), complete(), and fail() — but not from start(). This is because a job must exist and have a device token registered before push can be sent.

// Simple progress update
await job.setProgress(50);

// With message
await job.setProgress(50, { message: 'Processing frames...' });

// With stage info
await job.setProgress(66, {
  message: 'Rendering video...',
  stage: {
    name: 'rendering',
    current: 2,
    total: 3,
    description: 'Combining frames into video',
  },
});

// Fluent chaining
await job
  .setProgress(25, { message: 'Step 1...' })
  .then(j => j.setProgress(50, { message: 'Step 2...' }))
  .then(j => j.setProgress(75, { message: 'Step 3...' }));

#job.complete()

job.complete(options?: CompleteOptions): Promise<Job>

Mark job as completed with optional result data.

Options
result JobResult Result data { type?, url?, data? } (max 100KB)
message string Completion message
push PushOptions Push notification options (see Push Configuration)
// Simple completion
await job.complete();

// With result URL
await job.complete({
  result: {
    type: 'video',
    url: 'https://cdn.example.com/output.mp4',
  },
});

// With custom data
await job.complete({
  result: {
    type: 'analysis',
    data: {
      sentiment: 'positive',
      confidence: 0.95,
      keywords: ['happy', 'success'],
    },
  },
  message: 'Analysis complete!',
});

#job.fail()

job.fail(options: FailOptions): Promise<Job>

Mark job as failed with error details.

Options
error JobError Error details { code, message, details? }required
retryable boolean Whether the job can be retried (default: false)
push PushOptions Push notification options (see Push Configuration)
try {
  // Processing...
} catch (err) {
  await job.fail({
    error: {
      code: 'PROCESSING_ERROR',
      message: 'Failed to generate video',
      details: { reason: err.message },
    },
    retryable: true,
  });
}

#ETA (Estimated Time of Arrival)

Seenn automatically calculates ETAs based on historical job completion data. After 5+ completed jobs of the same type, ETAs become increasingly accurate.

#How It Works

  1. Job Creation: Backend calculates initial ETA from historical data (or uses default if <10 jobs)
  2. Progress Updates: ETA is recalculated based on current progress rate: remaining = (100 - progress) / rate
  3. Job Completion: Actual duration is recorded and used to improve future ETAs

#Workflow ID for Accuracy

ETAs are tracked per workflowId (or jobType as fallback). Use workflowId to segment ETAs when processing characteristics change:

// Different models have different processing times
const job = await seenn.jobs.start({
  userId: 'user_123',
  jobType: 'video-generation',
  workflowId: 'sora-v2',  // Track ETAs separately per model
  title: 'Creating video...',
});

// Without workflowId, jobType is used as ETA key
const job2 = await seenn.jobs.start({
  userId: 'user_123',
  jobType: 'image-upscale-4x',  // ETA tracked by jobType
  title: 'Upscaling image...',
});

// Set default ETA for new workflows (before 5 completions)
const job3 = await seenn.jobs.start({
  userId: 'user_123',
  jobType: 'video-generation',
  workflowId: 'new-model-v3',
  estimatedDuration: 180000,  // 3 min default until 5+ jobs complete
  title: 'Creating video...',
});

#ETA Fields on Job

Field Type Description
estimatedCompletionAt string? ISO 8601 timestamp for estimated completion
workflowId string? ETA tracking key (defaults to jobType if not provided)
etaConfidence number? Confidence score (0.0 - 1.0), higher = more historical data
etaBasedOn number? Number of historical jobs used for calculation
startedAt string? When the job started running (ISO 8601)

#Job Methods

const job = await seenn.jobs.get('job_123');

// Get remaining time in milliseconds
console.log(job.etaRemaining);  // 45000 (45 seconds)

// Check if job is running past ETA
console.log(job.isPastEta);     // true/false

// ETA metadata
console.log(job.etaConfidence); // 0.85 (85% confidence)
console.log(job.etaBasedOn);    // 42 (based on 42 historical jobs)

#ETA Service (Admin)

Manage ETA statistics for analytics and admin purposes:

// Get ETA stats for a workflow/jobType
const stats = await seenn.eta.getStats('sora-v2');  // etaKey = workflowId or jobType
console.log(stats);
// {
//   etaKey: 'sora-v2',
//   count: 156,
//   avgDuration: 120000,     // 2 minutes average
//   minDuration: 90000,      // fastest completion
//   maxDuration: 180000,     // slowest completion
//   defaultDuration: 120000, // from estimatedDuration param
//   confidence: 0.92,        // min(count/50, 1.0)
//   lastUpdated: '2026-01-23T...'
// }

// List all ETA stats
const allStats = await seenn.eta.list();

// Reset stats (e.g., after significant workflow changes)
await seenn.eta.reset('sora-v2');
Default ETA: Set default ETA via estimatedDuration param when creating jobs. This is used until 5+ jobs complete and historical data becomes available.
Mobile Countdown: The React Native SDK includes useEtaCountdown() hook that provides smooth countdown display with automatic sync to server ETA updates. See React Native SDK docs.

#Custom Backend

Using your own backend instead of Seenn Cloud? Configure the SDK to point to your API:

const seenn = new SeennClient({
  apiKey: process.env.SEENN_SECRET_KEY,
  // Point to your own backend
  baseUrl: 'https://api.yourcompany.com',
});
Open Source: The SDK is MIT licensed. You can use it with Seenn Cloud or your own backend implementation. Check GitHub for the API spec your backend needs to implement.

#Backend Requirements

Your self-hosted backend must implement these endpoints:

Endpoint Description
POST /v1/jobs Create a job (supports parent-child params)
GET /v1/jobs/:id Get job by ID
GET /v1/jobs/:parentId/children Get parent with all children
POST /v1/jobs/:id/progress Update job progress
POST /v1/jobs/:id/complete Mark job as completed
POST /v1/jobs/:id/fail Mark job as failed
GET /v1/eta List all ETA statistics (optional)
GET /v1/eta/:etaKey Get ETA stats for workflow/jobType (optional)
DELETE /v1/eta/:etaKey Reset ETA stats (optional)

#Error Handling

The SDK throws typed errors for different scenarios:

import {
  SeennError,
  ValidationError,
  NotFoundError,
  RateLimitError,
} from '@seenn/node';

try {
  await seenn.jobs.get('nonexistent');
} catch (err) {
  if (err instanceof NotFoundError) {
    console.log('Job not found');
  } else if (err instanceof RateLimitError) {
    console.log('Rate limited, retry after:', err.retryAfter);
  } else if (err instanceof ValidationError) {
    console.log('Invalid input:', err.message);
  }
}