Self-Hosted API Specification

Complete protocol specification for implementing your own Seenn-compatible backend

Open Source SDKs: Seenn's client and backend SDKs are MIT licensed and work with any backend that implements this protocol. You can use Seenn Cloud or build your own.

#Overview

To use Seenn SDKs with your own backend, you need to implement 6 REST endpoints and optionally an SSE endpoint for real-time updates.

Endpoint Method Description
/v1/jobs POST Create a new job
/v1/jobs/:id GET Get job by ID
/v1/jobs GET List jobs for a user
/v1/jobs/:id/progress POST Update job progress
/v1/jobs/:id/complete POST Mark job as completed
/v1/jobs/:id/fail POST Mark job as failed
/v1/jobs/:parentId/children GET Get parent job with children
/v1/sse GET SSE stream (optional)

#Authentication

All API requests require authentication via the Authorization header:

Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxx

#API Key Format

Seenn uses prefixed API keys for clarity:

Prefix Usage Permissions
sk_live_ Production secret key Full access (server-side only)
sk_test_ Development secret key Full access (test environment)
pk_live_ Production public key Read-only (safe for client-side)
pk_test_ Development public key Read-only (test environment)
Security: Never expose sk_* keys in client-side code. Use pk_* keys for mobile apps and browsers.

#POST /v1/jobs

Create a new job. Supports both regular jobs and parent-child relationships.

Request Body

{
  "jobType": "video-generation",        // required, 1-100 chars
  "userId": "user_123",                 // required, 1-255 chars
  "title": "Creating your video...",    // required, 1-200 chars
  "metadata": { "prompt": "..." },      // optional, max 10KB
  "queue": {                            // optional
    "position": 5,
    "total": 20,
    "queueName": "priority"
  },
  "stage": {                            // optional
    "name": "initializing",
    "current": 1,
    "total": 3,
    "description": "Setting up..."
  },
  "estimatedCompletionAt": "2026-01-22T15:00:00Z",  // optional, ISO 8601
  "ttlSeconds": 2592000,                // optional, 60-2592000 (30 days)

  // Parent-child fields (optional)
  "totalChildren": 5,                   // for parent jobs, 1-1000
  "childProgressMode": "average",       // 'average' | 'sequential' | 'weighted'
  "parentJobId": "01HXYZ...",           // for child jobs
  "childIndex": 0                       // for child jobs, 0-9999
}

Response (201 Created)

{
  "id": "01HXYZ123456789ABCDEFGHIJ",
  "appId": "app_xxx",
  "userId": "user_123",
  "jobType": "video-generation",
  "title": "Creating your video...",
  "status": "pending",
  "progress": 0,
  "metadata": { "prompt": "..." },
  "createdAt": "2026-01-22T14:30:00Z",
  "updatedAt": "2026-01-22T14:30:00Z",

  // For parent jobs
  "children": {
    "total": 5,
    "completed": 0,
    "failed": 0,
    "running": 0,
    "pending": 5
  },
  "childProgressMode": "average",

  // For child jobs
  "parent": {
    "parentJobId": "01HXYZ...",
    "childIndex": 0
  }
}

#GET /v1/jobs/:id

Get a job by its ID.

Response (200 OK)

Same format as POST response above.

#GET /v1/jobs

List jobs for a user with pagination.

Query Parameters

Parameter Type Description
userId string Required. User ID to list jobs for
limit number Max results (default: 20, max: 100)
cursor string Pagination cursor from previous response

Response (200 OK)

{
  "jobs": [
    {
      "id": "01HXYZ...",
      "userId": "user_123",
      "jobType": "video-generation",
      "title": "My Video",
      "status": "completed",
      "progress": 100,
      "createdAt": "2026-01-22T14:30:00Z",
      "updatedAt": "2026-01-22T14:35:00Z"
    }
  ],
  "nextCursor": "eyJsYXN0S2V5Ijo..."
}

#GET /v1/jobs/:parentId/children

Get a parent job with all its child jobs.

Response (200 OK)

{
  "parent": {
    "id": "01HXYZ...",
    "title": "Christmas Pack",
    "status": "running",
    "progress": 60,
    "children": {
      "total": 5,
      "completed": 3,
      "failed": 0,
      "running": 1,
      "pending": 1
    },
    "childProgressMode": "average"
  },
  "children": [
    {
      "id": "01HABC...",
      "childIndex": 0,
      "title": "Snowflake",
      "status": "completed",
      "progress": 100,
      "createdAt": "2026-01-22T14:30:00Z",
      "updatedAt": "2026-01-22T14:32:00Z",
      "completedAt": "2026-01-22T14:32:00Z"
    },
    {
      "id": "01HDEF...",
      "childIndex": 1,
      "title": "Christmas Tree",
      "status": "running",
      "progress": 50,
      "createdAt": "2026-01-22T14:30:00Z",
      "updatedAt": "2026-01-22T14:33:00Z"
    }
  ]
}

#POST /v1/jobs/:id/progress

Update job progress. Automatically transitions status from pending to running.

Request Body

{
  "progress": 50,                       // required, 0-100
  "message": "Processing frames...",    // optional
  "queue": { ... },                     // optional
  "stage": { ... },                     // optional
  "estimatedCompletionAt": "...",       // optional
  "metadata": { ... }                   // optional, merged with existing
}

Response (200 OK)

Returns the updated job object.

#POST /v1/jobs/:id/complete

Mark a job as completed.

Request Body

{
  "result": {                           // optional
    "type": "video",
    "url": "https://cdn.example.com/output.mp4",
    "data": { ... }
  },
  "message": "Video ready!"             // optional
}

Response (200 OK)

Returns the updated job with status: "completed", progress: 100, and completedAt timestamp.

#POST /v1/jobs/:id/fail

Mark a job as failed.

Request Body

{
  "error": {                            // required
    "code": "PROCESSING_ERROR",
    "message": "Failed to generate video",
    "details": { "reason": "..." }
  },
  "retryable": true                     // optional
}

Response (200 OK)

Returns the updated job with status: "failed" and the error object.

#Error Responses

All errors follow this format:

{
  "error": {
    "code": "NOT_FOUND",
    "message": "Job job_123 not found"
  }
}

Error Codes

HTTP Code Description
400 VALIDATION_ERROR Invalid request body or parameters
400 INVALID_JOB_STATE Can't perform action in current job state
400 INVALID_PARENT Parent job doesn't exist or isn't a parent
400 INVALID_CHILD_INDEX Child index exceeds parent's totalChildren
401 UNAUTHORIZED Invalid or missing API key
404 NOT_FOUND Job not found
429 RATE_LIMITED Too many requests
500 INTERNAL_ERROR Server error

#SSE Events (Optional)

For real-time updates, implement an SSE endpoint at /v1/sse.

Connection

GET /v1/sse?userId=user_123
Authorization: Bearer pk_live_xxx

Accept: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

Event Format

event: job.progress
id: 1705932600000
data: {"id":"01HXYZ...","status":"running","progress":50,...}

event: job.completed
id: 1705932660000
data: {"id":"01HXYZ...","status":"completed","progress":100,...}

event: heartbeat
id: 1705932615000
data: {}

Event Types

Event Description
connected Connection established
job.started New job created
job.progress Job progress updated
job.completed Job completed successfully
job.failed Job failed
parent.updated Parent job stats updated (child changed)
heartbeat Keep-alive (every 15s)

#Implementation Tips

Database Schema

Recommended fields for your jobs table:

CREATE TABLE jobs (
  id VARCHAR(26) PRIMARY KEY,           -- ULID
  app_id VARCHAR(100) NOT NULL,
  user_id VARCHAR(255) NOT NULL,
  job_type VARCHAR(100) NOT NULL,
  title VARCHAR(200) NOT NULL,
  status ENUM('pending','running','completed','failed'),
  progress INT DEFAULT 0,
  message TEXT,
  metadata JSON,
  queue JSON,
  stage JSON,
  result JSON,
  error JSON,
  estimated_completion_at TIMESTAMP,

  -- Parent-child
  parent_job_id VARCHAR(26),
  child_index INT,
  total_children INT,
  completed_children INT DEFAULT 0,
  failed_children INT DEFAULT 0,
  running_children INT DEFAULT 0,
  pending_children INT DEFAULT 0,
  child_progress_mode VARCHAR(20),

  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW(),
  completed_at TIMESTAMP,
  ttl TIMESTAMP,

  INDEX idx_user (app_id, user_id),
  INDEX idx_parent (parent_job_id)
);

Parent Progress Calculation

// When a child job updates, recalculate parent
function updateParentStats(parentId) {
  const children = getChildrenByParentId(parentId);
  const parent = getJob(parentId);

  let completed = 0, failed = 0, running = 0, pending = 0;
  let totalProgress = 0;

  for (const child of children) {
    switch (child.status) {
      case 'completed': completed++; totalProgress += 100; break;
      case 'failed': failed++; totalProgress += child.progress; break;
      case 'running': running++; totalProgress += child.progress; break;
      case 'pending': pending++; break;
    }
  }

  // Calculate progress based on mode
  let parentProgress;
  switch (parent.childProgressMode) {
    case 'average':
      parentProgress = Math.round(totalProgress / parent.totalChildren);
      break;
    case 'sequential':
      parentProgress = Math.round((completed / parent.totalChildren) * 100);
      break;
  }

  // Determine parent status
  let parentStatus = parent.status;
  if (running > 0 || (completed > 0 && completed < parent.totalChildren)) {
    parentStatus = 'running';
  } else if (completed === parent.totalChildren) {
    parentStatus = 'completed';
  } else if (failed > 0 && (completed + failed) === parent.totalChildren && completed === 0) {
    parentStatus = 'failed';
  }

  updateJob(parentId, {
    completedChildren: completed,
    failedChildren: failed,
    runningChildren: running,
    pendingChildren: pending,
    progress: parentProgress,
    status: parentStatus
  });
}