BaxStream Documentation

BaxStream Video Conversion API

Convert MP4, MOV, AVI, MKV, WebM, FLV and WMV to adaptive HLS streaming with a single API call. Multi-quality output delivered to your S3 bucket.

Prerequisites

You need an API Key and a Project ID to use BaxStream.

API Key — Create one in Dashboard → API Keys. Your API key must have BaxStream permission enabled (toggle it when creating the key).

Project ID — Find it in Dashboard → Projects → [Your Project]. All BaxStream requests are scoped to a project.

S3 Config — Either configure default S3 settings on your project (Project Settings → BaxStream → S3 Configuration) or pass s3Config per request.

API Key

Bearer token for all requests

bax_xxxxxxxxxxxxxxxx

Project ID

Scopes jobs, billing, analytics

clxxxxxxxxxxxxxx

S3 Bucket

Where converted files land

my-videos-bucket

Authentication

All requests to api.baxcloud.tech require two headers:

Authorization
header
required

Bearer YOUR_API_KEY — your BaxCloud API key with BaxStream permission enabled.

X-Project-Id
header
required

Your project ID. All jobs are scoped to this project for billing and analytics.

example headers
Authorization: Bearer bax_xxxxxxxxxxxxxxxx
X-Project-Id: clxxxxxxxxxxxxxx

Quick Start

Submit a conversion job with a single POST. Replace YOUR_API_KEY and YOUR_PROJECT_ID with your actual values.

curl
curl -X POST \
  https://api.baxcloud.tech/video/convert \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "X-Project-Id: YOUR_PROJECT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "inputUrl": "https://example.com/my-video.mp4",
    "outputFormats": ["360p", "480p", "720p"],
    "categorize": true,
    "moderate": true,
    "metadata": { "userId": "u_42", "uploadId": "u_42_v17" },
    "s3Config": {
      "bucket": "my-bucket",
      "region": "us-east-1",
      "prefix": "videos/my-video/",
      "accessKey": "AKIA...",
      "secretKey": "...",
      "cdnUrl": "https://cdn.example.com"
    }
  }'

Response:

response.json
{
  "id": "cm...",
  "projectId": "cl...",
  "status": "PENDING",
  "inputUrl": "https://example.com/my-video.mp4",
  "outputFormats": ["360p", "480p", "720p"],
  "metadata": { "userId": "u_42", "uploadId": "u_42_v17" },
  "createdAt": "2025-04-16T00:00:00.000Z"
}

Create Job (from URL)

POST
https://api.baxcloud.tech/video/convert

Request Body

inputUrl
string
required

URL of the source video. Supported formats: MP4, MOV, AVI, MKV, WebM, FLV, WMV, M4V, TS, MTS, 3GP, OGV.

outputFormats
string[]

Quality presets to generate. Options: 360p, 480p, 720p, 1080p. Default: ["360p", "480p", "720p"].

ffmpegPreset
string

Encoding speed/quality trade-off. Options: ultrafast, veryfast, fast (default), medium, slow, veryslow. Slower presets produce smaller files at the same quality.

hlsTime
number

HLS segment duration in seconds. Default: 2. Range: 1–10. Shorter segments = faster seek, slightly larger overhead.

s3Config
object

S3 output configuration. Optional if project defaults are configured.

s3Config.bucket
string
required

S3 bucket name.

s3Config.region
string

AWS region (e.g. us-east-1). Default: auto.

s3Config.endpoint
string

Custom S3 endpoint for S3-compatible storage (Cloudflare R2, MinIO, etc).

s3Config.prefix
string

Key prefix for output files (e.g. videos/my-video/). Default: /.

s3Config.accessKey
string
required

S3 access key ID.

s3Config.secretKey
string
required

S3 secret access key.

s3Config.cdnUrl
string

Public / CDN base URL. When set, all output URLs (playlists, thumbnails) use this domain instead of the raw S3 URL. Keeps your S3 endpoint private.

categorize
boolean

Also run AI video categorization alongside HLS conversion. Default: false. Results included in webhook payload.

moderate
boolean

Also run AI content moderation alongside HLS conversion. Default: false. Results included in webhook payload — you decide what to do with flagged content.

metadata
object

Arbitrary JSON metadata to attach to the job. Returned in webhooks and API responses.

Upload File (multipart)

POST
https://api.baxcloud.tech/video/upload

Upload a video file directly instead of providing a URL. Max file size: 2 GB. Send as multipart/form-data.

curl
curl -X POST \
  https://api.baxcloud.tech/video/upload \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "X-Project-Id: YOUR_PROJECT_ID" \
  -F "video=@./my-video.mp4" \
  -F 'outputFormats=["360p","720p"]' \
  -F 'ffmpegPreset=fast' \
  -F 'metadata={"userId":"u_42"}'

Form Fields

video
file
required

The video file to convert.

outputFormats
string (JSON)

JSON array of quality presets, e.g. ["360p","720p"].

ffmpegPreset
string

Encoding preset. Same options as the JSON API.

hlsTime
string

HLS segment duration (as string).

s3Config
string (JSON)

JSON object with S3 configuration. Same schema as the JSON API.

metadata
string (JSON)

JSON metadata object.

List Jobs

GET
https://api.baxcloud.tech/video/jobs

Query Parameters

status
string

Filter by status: PENDING, PROCESSING, COMPLETED, FAILED, CANCELLED.

page
number

Page number (default: 1).

pageSize
number

Items per page (default: 20, max: 100).

Get Job

GET
https://api.baxcloud.tech/video/jobs/:jobId

Returns the full job object including output URLs when completed.

curl
curl https://api.baxcloud.tech/video/jobs/JOB_ID \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "X-Project-Id: YOUR_PROJECT_ID"

Response (completed):

response.json (completed)
{
  "id": "cm...",
  "status": "COMPLETED",
  "inputUrl": "https://example.com/video.mp4",
  "outputUrl": "https://cdn.example.com/videos/master.m3u8",
  "thumbnailUrl": "https://cdn.example.com/videos/thumbnail.jpg",
  "outputFormats": ["360p", "480p", "720p"],
  "durationSec": 125,
  "segmentsCount": 63,
  "costCents": 13,
  "metadata": { "userId": "u_42" },
  "createdAt": "2025-04-16T00:00:00.000Z",
  "startedAt": "2025-04-16T00:00:02.000Z",
  "completedAt": "2025-04-16T00:01:30.000Z"
}

Cancel Job

DELETE
https://api.baxcloud.tech/video/jobs/:jobId

Only PENDING jobs can be cancelled. Returns 204 No Content on success.

Quality Presets

BaxStream generates adaptive HLS with the quality presets you select. The video is scaled to fit within the preset dimensions while preserving the original aspect ratio. Videos are never upscaled beyond their source resolution.

PresetMax ResolutionVideo BitrateAudio Bitrate
360p640 × 360800 kbps96 kbps
480p854 × 4801400 kbps128 kbps
720p1280 × 7202800 kbps128 kbps
1080p1920 × 10805000 kbps192 kbps

The default output is ["360p", "480p", "720p"]. Add "1080p" explicitly if needed. Portrait and non-standard aspect ratios are fully supported.

S3 Configuration

BaxStream uploads converted files directly to your S3-compatible storage. You can configure defaults at the project level or pass them per request.

Supported Providers

AWS S3
Cloudflare R2
DigitalOcean Spaces
MinIO
Backblaze B2
Wasabi

CDN / Public URL

Set cdnUrl (or configure it in Project Settings) to serve output files through your CDN. This replaces the raw S3 URL in all output references, keeping your bucket credentials private.

example
"s3Config": {
  "bucket": "my-bucket",
  "region": "us-east-1",
  "accessKey": "AKIA...",
  "secretKey": "...",
  "cdnUrl": "https://cdn.example.com"
}

// Output URL becomes:
// https://cdn.example.com/videos/master.m3u8
// instead of:
// https://my-bucket.s3.us-east-1.amazonaws.com/videos/master.m3u8

Content Moderation

BaxStream includes AI-powered content moderation backed by our vision AI. You can run moderation standalone or alongside HLS conversion. Moderation results are always returned to you — you decide what to do with flagged content.

Standalone Moderation

POST
https://api.baxcloud.tech/video/moderate

inputUrl
string
required

URL of the video or image to moderate.

inputType
string

video (default) or image.

maxFrames
number

Max frames to extract from video (1–16). Default: 8.

threshold
number

Score threshold for flagging (0–1). Default: 0.30. Lower = more sensitive.

metadata
object

Arbitrary JSON metadata to attach to the job.

curl
curl -X POST \
  https://api.baxcloud.tech/video/moderate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "X-Project-Id: YOUR_PROJECT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "inputUrl": "https://example.com/video.mp4",
    "maxFrames": 8,
    "threshold": 0.30,
    "metadata": { "userId": "u_42" }
  }'

Safe response:

response.json (safe)
{
  "success": true,
  "data": {
    "id": "cm...",
    "status": "COMPLETED",
    "safe": true,
    "violations": [],
    "scores": {
      "nudity_explicit":    0.002,
      "sexual_activity":    0.001,
      "suggestive_nudity":  0.010,
      "lingerie_underwear": 0.008,
      "sexy_suggestive":    0.015,
      "violence":           0.020,
      "gore_graphic":       0.001,
      "hate_symbols":       0.000,
      "drugs_substances":   0.004,
      "weapons":            0.002,
      "child_safety":       0.000,
      "self_harm":          0.000,
      "harassment":         0.003
    },
    "flaggedFrames": [],
    "framesAnalyzed": 8,
    "processingTimeMs": 1250
  }
}

Flagged response (adult content example):

response.json (flagged)
{
  "success": true,
  "data": {
    "id": "cm...",
    "status": "COMPLETED",
    "safe": false,
    "violations": [
      { "category": "nudity_explicit",    "confidence": 0.5210, "severity": "critical" },
      { "category": "suggestive_nudity",  "confidence": 0.3104, "severity": "high" },
      { "category": "lingerie_underwear", "confidence": 0.2850, "severity": "medium" }
    ],
    "scores": {
      "nudity_explicit":    0.4812,
      "sexual_activity":    0.0832,
      "suggestive_nudity":  0.2901,
      "lingerie_underwear": 0.2684,
      "sexy_suggestive":    0.0914,
      "violence":           0.0014,
      "weapons":            0.0008
      // ... all other categories
    },
    "flaggedFrames": [
      {
        "frameIndex": 2,
        "violations": [
          { "category": "nudity_explicit", "confidence": 0.5834, "severity": "critical" }
        ]
      },
      {
        "frameIndex": 5,
        "violations": [
          { "category": "suggestive_nudity", "confidence": 0.3901, "severity": "high" }
        ]
      }
    ],
    "framesAnalyzed": 8,
    "processingTimeMs": 1250
  }
}

Moderation Categories

CategorySeverityDescription
nudity_explicitcriticalFull nudity with visible genitals, hardcore pornographic imagery
sexual_activitycriticalPeople engaged in sexual intercourse or explicit sexual acts
suggestive_nudityhighTopless, exposed buttocks, implied nudity from behind
lingerie_underwearmediumLingerie, bra & panties, revealing underwear, boudoir shoots
sexy_suggestivelowSensual dancing, seductive poses, tight/revealing clothing
violencehighFighting, physical assault, armed conflict, war footage
gore_graphiccriticalExtremely graphic violence, mutilation, corpses
hate_symbolscriticalHate group symbols, racist imagery, propaganda
drugs_substancesmediumDrug use, paraphernalia, substance abuse
weaponsmediumFirearms, knives, explosives displayed prominently
child_safetycriticalContent endangering or exploiting minors
self_harmcriticalSelf-injury, suicidal imagery, pro-anorexia
harassmentmediumBullying, threatening, intimidating behavior

List Moderation Jobs

GET
https://api.baxcloud.tech/video/moderate/jobs

Get Moderation Job

GET
https://api.baxcloud.tech/video/moderate/jobs/:jobId

Inline with Conversion

Add "moderate": true to your conversion request. Moderation results will be included in the video.conversion.completed webhook payload under the moderation key.

curl
curl -X POST \
  https://api.baxcloud.tech/video/convert \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "X-Project-Id: YOUR_PROJECT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "inputUrl": "https://example.com/video.mp4",
    "outputFormats": ["360p", "720p"],
    "moderate": true,
    "s3Config": { "bucket": "my-bucket" }
  }'

Standalone AI Endpoints

Use these clean endpoints to run AI categorization and moderation independently of video conversion. The same authentication applies: Authorization: Bearer YOUR_API_KEY + X-Project-Id header.

These endpoints are billed per job.

Each plan includes a number of categorization and moderation jobs. Usage beyond included jobs is billed at your plan's overage rate per job. Check your plan details in Dashboard → Billing.

Standalone AI is synchronous — await the HTTP response

POST /categorize/*, POST /moderate/image, POST /moderate/images, and upload variants block until the CLIP worker finishes. The JSON response contains status: "COMPLETED" with full results (safe, violations, categories, batchItems, etc.). You do not need a webhook or a follow-up GET to read the verdict.

Webhooks (moderation.completed, etc.) still fire if configured — useful for logging or multi-service pipelines — but optional when you call the API directly. HLS conversion remains async; see Webhooks.

Categorize Video

POST
https://api.baxcloud.tech/categorize/video

inputUrl
string
required

URL of the video to categorize.

metadata
object

Arbitrary JSON metadata to attach to the job.

curl
curl -X POST \
  https://api.baxcloud.tech/categorize/video \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "X-Project-Id: YOUR_PROJECT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "inputUrl": "https://example.com/video.mp4",
    "topK": 6,
    "maxFrames": 8,
    "metadata": { "userId": "u_42" }
  }'

Categorize Image

POST
https://api.baxcloud.tech/categorize/image

inputUrl
string
required

URL of the image to categorize.

metadata
object

Arbitrary JSON metadata to attach to the job.

curl
curl -X POST \
  https://api.baxcloud.tech/categorize/image \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "X-Project-Id: YOUR_PROJECT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "inputUrl": "https://example.com/photo.jpg",
    "topK": 6
  }'

Categorization Response:

The categories array always starts with the primary category (the highest-scoring one, marked primary: true) followed by up to 5 other categories with confidence ≥ 1%. Categories below 1% are dropped from categories but are still available in the scores object, which contains the full probability map across all 20 supported categories.

Use topK (1–20, default 6) to change the maximum total entries returned in categories.

response.json (categorization)
{
  "success": true,
  "data": {
    "id": "cm...",
    "status": "COMPLETED",
    "primaryCategory": "Sports",
    "confidence": 0.4523,
    "categories": [
      { "name": "Sports",            "confidence": 0.4523, "primary": true  },
      { "name": "Film & TV",         "confidence": 0.0812, "primary": false },
      { "name": "Travel & Nature",   "confidence": 0.0340, "primary": false },
      { "name": "Lifestyle & Vlog",  "confidence": 0.0185, "primary": false }
    ],
    "tags": ["football", "stadium", "athletes running", "crowd cheering"],
    "scores": {
      "Sports":           0.4523,
      "Film & TV":        0.0812,
      "Travel & Nature":  0.0340,
      "Lifestyle & Vlog": 0.0185,
      "Comedy & Memes":   0.0091
      // ... full long-tail of all 23 categories
    },
    "framesAnalyzed": 8,
    "processingTimeMs": 980
  }
}

Moderate Video

POST
https://api.baxcloud.tech/moderate/video

inputUrl
string
required

URL of the video to moderate.

maxFrames
number

Max frames to extract from video (1–16). Default: 8.

threshold
number

Score threshold for flagging (0–1). Default: 0.30. Lower = more sensitive.

metadata
object

Arbitrary JSON metadata to attach to the job.

curl
curl -X POST \
  https://api.baxcloud.tech/moderate/video \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "X-Project-Id: YOUR_PROJECT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "inputUrl": "https://example.com/video.mp4",
    "maxFrames": 8,
    "threshold": 0.30,
    "metadata": { "userId": "u_42" }
  }'

Moderate Image

POST
https://api.baxcloud.tech/moderate/image synchronous. The HTTP response returns status: "COMPLETED" with safe, violations, and related fields. Webhooks are optional.

inputUrl
string
required

URL of the image to moderate.

threshold
number

Score threshold for flagging (0–1). Default: 0.30. Lower = more sensitive.

metadata
object

Arbitrary JSON metadata to attach to the job.

curl
curl -X POST \
  https://api.baxcloud.tech/moderate/image \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "X-Project-Id: YOUR_PROJECT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "inputUrl": "https://example.com/photo.jpg",
    "threshold": 0.3
  }'
POST /moderate/image — HTTP response (single image)
{
  "success": true,
  "data": {
    "id": "cm_mod_img789",
    "status": "COMPLETED",
    "inputUrl": "https://example.com/photo.jpg",
    "inputType": "image",
    "safe": true,
    "violations": [],
    "scores": { "violence": 0.02, "suggestive_nudity": 0.01 },
    "flaggedFrames": [],
    "framesAnalyzed": 1,
    "processingTimeMs": 340,
    "createdAt": "2026-04-17T10:02:09.000Z",
    "completedAt": "2026-04-17T10:02:10.000Z"
  }
}

Upload Image for Moderation

POST
https://api.baxcloud.tech/moderate/image/upload — multipart field image (max 20 MB). Also synchronous — same completed job in the response. Optional form fields: threshold, metadata (JSON string).

curl
curl -X POST \
  https://api.baxcloud.tech/moderate/image/upload \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "X-Project-Id: YOUR_PROJECT_ID" \
  -F "image=@photo.jpg" \
  -F "threshold=0.3"

Moderate Multiple Images (batch)

POST
https://api.baxcloud.tech/moderate/images — pass inputUrls (1–20 publicly reachable image URLs). Returns one job with aggregated results and per-image entries in webhooks under data.items[].

curl
curl -X POST \
  https://api.baxcloud.tech/moderate/images \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "X-Project-Id: YOUR_PROJECT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "inputUrls": [
      "https://example.com/a.jpg",
      "https://example.com/b.jpg"
    ],
    "threshold": 0.3
  }'

Upload Multiple Images for Moderation

POST
https://api.baxcloud.tech/moderate/images/upload — multipart field images (repeat for each file, 1–20 images, max 20 MB each).

curl
curl -X POST \
  https://api.baxcloud.tech/moderate/images/upload \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "X-Project-Id: YOUR_PROJECT_ID" \
  -F "images=@a.jpg" \
  -F "images=@b.jpg" \
  -F "threshold=0.3"

Supported formats & URL requirements

  • Images: JPEG, PNG, GIF, WebP, BMP, TIFF
  • URL jobs: inputUrl must be publicly reachable (HTTP 200). Extensionless URLs (e.g. signed CDN links) work when you set inputType: "image" or use POST /moderate/image — the worker also sniffs Content-Type and magic bytes after download.
  • Image responses: framesAnalyzed is 1 per image; flaggedFrames uses frameIndex: 0 for single-image jobs. Batch jobs include items[] with one entry per image.

Sync API vs webhooks

FlowExecutionHow to get results
Standalone categorization & moderationSynchronousHTTP response (status: COMPLETED). Webhooks optional.
HLS video conversionAsync workerPoll GET …/jobs/:id or video.conversion.* webhooks
Conversion + inline moderateAsyncvideo.conversion.completed includes nested moderation

Standalone moderation and categorization jobs are synchronous: the HTTP response includes the final COMPLETED job (or a structured error). BaxStream also fires moderation.started, moderation.completed, and moderation.failed webhooks (same for categorization.*) if you subscribe — useful for audit trails, but not required when you await the create/upload call directly. Subscribe in Dashboard → Webhooks and verify signatures with the Server SDK.

Image moderation responses (single or batch) include batchItems in the job record and items[] in webhook payloads — same shape: per-image safe, violations, scores, and optional errorMessage. Top-level fields are aggregated across the batch (safe is false if any image is flagged).

POST /moderate/images — HTTP response (batch)
{
  "success": true,
  "data": {
    "id": "cm_mod_batch789",
    "status": "COMPLETED",
    "inputUrl": "https://cdn.example.com/a.jpg",
    "inputType": "image",
    "safe": false,
    "violations": [{ "category": "violence", "confidence": 0.51, "severity": "high" }],
    "framesAnalyzed": 2,
    "processingTimeMs": 680,
    "batchItems": [
      {
        "index": 0,
        "inputUrl": "https://cdn.example.com/a.jpg",
        "safe": true,
        "violations": [],
        "scores": {},
        "flaggedFrames": [],
        "framesAnalyzed": 1,
        "processingTimeMs": 320
      },
      {
        "index": 1,
        "inputUrl": "https://cdn.example.com/b.jpg",
        "safe": false,
        "violations": [{ "category": "violence", "confidence": 0.51, "severity": "high" }],
        "scores": { "violence": 0.51 },
        "flaggedFrames": [{ "frameIndex": 0, "violations": [...] }],
        "framesAnalyzed": 1,
        "processingTimeMs": 360
      }
    ],
    "metadata": { "batchSize": 2 }
  }
}

Production disclaimer

BaxStream moderation uses zero-shot vision AI (CLIP). It helps flag potentially harmful content but is not a legal compliance guarantee and may produce false positives or false negatives. Always apply your own policy and human review for high-risk use cases.

Error codes

Failed BaxStream API calls return a structured JSON envelope. Use error.code for programmatic handling and error.details.helpUrl to link users to the right dashboard page.

{
  "success": false,
  "error": {
    "code": "BAXVERIFY_FEATURE_DISABLED",
    "message": "BaxVerify is not enabled for this project. Enable it under Project → Features.",
    "details": {
      "projectId": "your-project-id",
      "helpUrl": "https://baxcloud.tech/dashboard/projects/your-project-id?tab=features",
      "docsUrl": "https://baxcloud.tech/docs/baxverify"
    }
  },
  "timestamp": "2026-06-06T12:00:00.000Z",
  "path": "/v1/auth/sms/send"
}
CodeHTTPMessageWhat to do
BAXSTREAM_MODERATION_PLAN_NOT_INCLUDED403Your plan does not include AI content moderation.Upgrade your subscription (details.helpUrl → Billing).
BAXSTREAM_MODERATION_INPUT_INVALID400Could not read the image or video at inputUrl.Use a supported format (JPEG, PNG, GIF, WebP, BMP, TIFF) and a valid public URL, or upload via POST /moderate/image/upload.
BAXSTREAM_MODERATION_INPUT_UNREACHABLE502Could not download inputUrl.Ensure the URL is publicly reachable and returns HTTP 200.
BAXSTREAM_MODERATION_WORKER_UNAVAILABLE503AI analysis service is temporarily unavailable.Retry after a short delay.
BAXSTREAM_MODERATION_INVALID_IMAGE_TYPE400Uploaded file is not a supported image type.Send JPEG, PNG, GIF, WebP, BMP, or TIFF via multipart field "image".
BAXSTREAM_MODERATION_NO_FILE400No image file uploaded.POST multipart/form-data with field name "image".
BAXSTREAM_MODERATION_FAILED400Moderation job failed.Check inputUrl, plan limits, and retry. See docs for details.helpUrl.
BAXSTREAM_PROJECT_INACTIVE403Project is not active.Reactivate the project in the dashboard.

List & Get AI Jobs

Use the following endpoints to list and retrieve job details:

MethodPathDescription
GET/categorize/jobsList categorization jobs (supports ?status=, ?page=, ?pageSize=)
GET/categorize/jobs/:jobIdGet a specific categorization job
GET/moderate/jobsList moderation jobs
GET/moderate/jobs/:jobIdGet a specific moderation job

Legacy endpoints still available

The original endpoints /video/categorize and /video/moderate continue to work. The new /categorize/* and /moderate/* paths are recommended for standalone AI usage since they clearly separate AI features from video conversion.

Webhooks

Webhooks push job lifecycle events to your backend in real time — especially important for async HLS conversion. For standalone AI, the HTTP response already contains results; webhooks are an optional duplicate. Configure endpoints in Dashboard → Webhooks, subscribe to events, and BaxStream will POST a signed JSON payload every time a matching event fires.

Signed

HMAC-SHA256 over the raw body, verified with your secret

At-least-once

Use eventId to dedupe; respond 2xx within 10s

Rich payload

Full job, AI categorization & moderation results, your metadata

Event Types

BaxStream emits 11 events across 3 lifecycles. *.started events fire when standalone AI jobs begin,*.completed on success, and *.failed on terminal errors. Video conversion usesvideo.conversion.* (async worker pipeline).

EventWhen it firesNotable payload fields
video.conversion.startedWorker began transcodingjobId, inputUrl, outputFormats
video.conversion.completedAll HLS renditions uploaded successfullyoutputUrl, thumbnailUrl, renditionUrls, durationSec, costCents, plus categorization / moderation when those features are enabled
video.conversion.failedConversion failed after retrieserrorMessage, errorCode
categorization.startedAI categorization began (standalone job)jobId, inputUrl
categorization.completedCategorization finishedprimaryCategory, categories[] (1 primary + others ≥1%), scores, tags
categorization.failedCategorization failederrorMessage
moderation.startedAI moderation began (standalone job)jobId, inputUrl, inputType, status, createdAt; batch jobs add inputUrls, itemCount
moderation.completedModeration finishedinputType, aggregate safe, violations[], flaggedFrames, scores, framesAnalyzed, processingTimeMs; image jobs also include items[] (per-image results). Batch jobs add inputUrls, itemCount.
moderation.failedModeration failedstatus (FAILED), errorMessage, jobId, inputType; batch/image jobs may include items[] with per-image errorMessage

Envelope Shape

Every webhook delivery uses the same outer envelope. The job-specific body lives under data; your user-supplied metadata is also hoisted to the top level for convenience.

envelope.json
{
  "event":     "video.conversion.completed",   // one of the 11 event types
  "eventId":   "cm9a8w1...",                   // unique id, use it to dedupe
  "timestamp": "2026-04-17T10:00:00.000Z",     // ISO-8601 server time of delivery
  "projectId": "cl_proj_abc123",               // your project id
  "metadata":  { "userId": "u_42" },           // user metadata you passed at create-time (or null)
  "data":      { /* event-specific job payload, see examples below */ }
}

Why both top-level and nested metadata?

The hoisted top-level metadata lets your handler read it without first parsing the full data body — useful for routing or fast-path filtering. The nested copy inside data.metadata is preserved so older integrations keep working.

Payload: video.conversion.completed

Fired when HLS conversion finishes. data.categorization is included when you sent categorize: true, data.moderation when you sent moderate: true.

video.conversion.completed
{
  "event": "video.conversion.completed",
  "eventId": "cm9a8w1xyz...",
  "timestamp": "2026-04-17T10:00:00.000Z",
  "projectId": "cl_proj_abc123",
  "metadata": { "userId": "u_42", "uploadId": "u_42_v17" },
  "data": {
    "jobId": "cm_job_xyz789",
    "projectId": "cl_proj_abc123",
    "status": "COMPLETED",
    "inputUrl": "https://source.example.com/video.mp4",
    "outputUrl": "https://cdn.example.com/videos/master.m3u8",
    "thumbnailUrl": "https://cdn.example.com/videos/thumbnail.jpg",
    "outputFormats": ["360p", "480p", "720p"],
    "renditionUrls": {
      "360p": "https://cdn.example.com/videos/360p/playlist.m3u8",
      "480p": "https://cdn.example.com/videos/480p/playlist.m3u8",
      "720p": "https://cdn.example.com/videos/720p/playlist.m3u8"
    },
    "durationSec": 125,
    "segmentsCount": 63,
    "costCents": 13,
    "metadata": { "userId": "u_42", "uploadId": "u_42_v17" },
    "createdAt": "2026-04-17T09:58:12.000Z",
    "startedAt": "2026-04-17T09:58:14.000Z",
    "completedAt": "2026-04-17T10:00:00.000Z",

    // Included when categorize=true (1 primary + up to 5 others ≥ 1%)
    "categorization": {
      "primaryCategory": "Sports",
      "confidence": 0.4523,
      "categories": [
        { "name": "Sports",         "confidence": 0.4523, "primary": true  },
        { "name": "Film & TV",      "confidence": 0.0812, "primary": false },
        { "name": "Travel & Nature","confidence": 0.0340, "primary": false }
      ],
      "tags": ["football", "stadium", "athletes running"],
      "scores": { "Sports": 0.4523, "Film & TV": 0.0812, /* ... full long-tail */ },
      "framesAnalyzed": 8,
      "processingTimeMs": 750
    },

    // Included when moderate=true (granular sexual-content + other categories)
    "moderation": {
      "safe": true,
      "violations": [],
      "scores": {
        "nudity_explicit":    0.002, "sexual_activity":    0.001,
        "suggestive_nudity":  0.010, "lingerie_underwear": 0.008,
        "sexy_suggestive":    0.015, "violence":           0.020,
        "gore_graphic":       0.001, "weapons":            0.002
        // ... all 13 moderation categories
      },
      "flaggedFrames": [],
      "framesAnalyzed": 8,
      "processingTimeMs": 980
    }
  }
}

Standalone moderation webhooks

Fired by standalone POST /moderate, POST /moderate/image, POST /moderate/images, POST /moderate/image/upload, or POST /moderate/images/upload jobs (not inline HLS conversion). All three events share the same data field set; result fields (safe, violations, etc.) are present on *.completed and omitted or empty on *.started. Check data.inputType to distinguish image (framesAnalyzed: 1, flaggedFrames[].frameIndex: 0) from video (up to maxFrames, default 8).

FieldTypePresent on
jobIdstringall events
projectIdstringall events
statusPENDING | COMPLETED | FAILEDall events
inputUrlstringall events
inputType"video" | "image"all events
safeboolean*.completed only
violations[{ category, confidence, severity }] *.completed (empty when safe)
scores{ category: number } (13 categories)*.completed only
flaggedFrames[{ frameIndex, violations[] }] *.completed (empty when safe)
framesAnalyzedinteger*.completed1 for images, up to maxFrames for video
processingTimeMsinteger*.completed only
errorMessagestring*.failed only
metadataobject | nullall events (also hoisted to envelope root)
createdAtISO-8601all events
completedAtISO-8601 | null*.completed and *.failed

Payload: moderation.started

Fires immediately when the job is accepted, before the CLIP worker runs. data.status is PENDING; moderation result fields are not populated yet.

moderation.started
{
  "event": "moderation.started",
  "eventId": "cm9a8w2start...",
  "timestamp": "2026-04-17T10:01:40.000Z",
  "projectId": "cl_proj_abc123",
  "metadata": { "userId": "u_42", "assetId": "img_991" },
  "data": {
    "jobId": "cm_mod_def456",
    "projectId": "cl_proj_abc123",
    "status": "PENDING",
    "inputUrl": "https://cdn.example.com/uploads/photo.webp",
    "inputType": "image",
    "safe": true,
    "framesAnalyzed": 0,
    "metadata": { "userId": "u_42", "assetId": "img_991" },
    "createdAt": "2026-04-17T10:01:40.000Z",
    "completedAt": null
  }
}

Batch image jobs include inputUrls and itemCount on moderation.started as well; per-image items[] appear on moderation.completed / moderation.failed.

Payload: moderation.completed (safe, video)

moderation.completed — safe video
{
  "event": "moderation.completed",
  "eventId": "cm9a8w2safe...",
  "timestamp": "2026-04-17T10:01:55.000Z",
  "projectId": "cl_proj_abc123",
  "metadata": { "userId": "u_42" },
  "data": {
    "jobId": "cm_mod_vid001",
    "projectId": "cl_proj_abc123",
    "status": "COMPLETED",
    "inputUrl": "https://source.example.com/clean-clip.mp4",
    "inputType": "video",
    "safe": true,
    "violations": [],
    "scores": {
      "nudity_explicit": 0.002, "sexual_activity": 0.001,
      "suggestive_nudity": 0.008, "lingerie_underwear": 0.006,
      "sexy_suggestive": 0.012, "violence": 0.018,
      "gore_graphic": 0.001, "weapons": 0.003
      // ... all 13 moderation categories
    },
    "flaggedFrames": [],
    "framesAnalyzed": 8,
    "processingTimeMs": 920,
    "metadata": { "userId": "u_42" },
    "createdAt": "2026-04-17T10:01:40.000Z",
    "completedAt": "2026-04-17T10:01:55.000Z"
  }
}

Payload: moderation.completed (flagged, video)

moderation.completed — flagged video
{
  "event": "moderation.completed",
  "eventId": "cm9a8w2abc...",
  "timestamp": "2026-04-17T10:01:42.000Z",
  "projectId": "cl_proj_abc123",
  "metadata": { "userId": "u_42" },
  "data": {
    "jobId": "cm_mod_def456",
    "projectId": "cl_proj_abc123",
    "status": "COMPLETED",
    "inputUrl": "https://source.example.com/risky.mp4",
    "inputType": "video",
    "safe": false,
    "violations": [
      { "category": "nudity_explicit",    "confidence": 0.5210, "severity": "critical" },
      { "category": "suggestive_nudity",  "confidence": 0.3104, "severity": "high" },
      { "category": "lingerie_underwear", "confidence": 0.2850, "severity": "medium" }
    ],
    "scores": { /* full per-category map — same keys as safe example */ },
    "flaggedFrames": [
      { "frameIndex": 2, "violations": [
        { "category": "nudity_explicit", "confidence": 0.5834, "severity": "critical" }
      ]},
      { "frameIndex": 5, "violations": [
        { "category": "suggestive_nudity", "confidence": 0.3901, "severity": "high" }
      ]}
    ],
    "framesAnalyzed": 8,
    "processingTimeMs": 1180,
    "metadata": { "userId": "u_42" },
    "createdAt": "2026-04-17T10:01:40.000Z",
    "completedAt": "2026-04-17T10:01:42.000Z"
  }
}

Payload: moderation.completed (flagged, image)

Images always analyze a single frame: framesAnalyzed is 1 and any flagged frame uses frameIndex: 0.

moderation.completed — flagged image
{
  "event": "moderation.completed",
  "eventId": "cm9a8w2img...",
  "timestamp": "2026-04-17T10:02:10.000Z",
  "projectId": "cl_proj_abc123",
  "metadata": { "userId": "u_42", "assetId": "img_991" },
  "data": {
    "jobId": "cm_mod_img789",
    "projectId": "cl_proj_abc123",
    "status": "COMPLETED",
    "inputUrl": "https://cdn.example.com/uploads/photo.webp",
    "inputType": "image",
    "safe": false,
    "violations": [
      { "category": "suggestive_nudity", "confidence": 0.4120, "severity": "high" }
    ],
    "scores": { /* full per-category map */ },
    "flaggedFrames": [
      { "frameIndex": 0, "violations": [
        { "category": "suggestive_nudity", "confidence": 0.4120, "severity": "high" }
      ]}
    ],
    "framesAnalyzed": 1,
    "processingTimeMs": 340,
    "items": [
      {
        "index": 0,
        "inputUrl": "https://cdn.example.com/uploads/photo.webp",
        "safe": false,
        "violations": [
          { "category": "suggestive_nudity", "confidence": 0.4120, "severity": "high" }
        ],
        "scores": { /* per-category map for this image */ },
        "flaggedFrames": [
          { "frameIndex": 0, "violations": [
            { "category": "suggestive_nudity", "confidence": 0.4120, "severity": "high" }
          ]}
        ],
        "framesAnalyzed": 1,
        "processingTimeMs": 340
      }
    ],
    "metadata": { "userId": "u_42", "assetId": "img_991" },
    "createdAt": "2026-04-17T10:02:09.000Z",
    "completedAt": "2026-04-17T10:02:10.000Z"
  }
}

Payload: moderation.completed (batch images, 2–20)

When you submit multiple image URLs via POST /moderate/images or POST /moderate/images/upload, top-level fields are aggregated across all images (job is safe: false if any image is flagged). Use data.items[] for per-image verdicts.

moderation.completed — batch images
{
  "event": "moderation.completed",
  "eventId": "cm9a8w2batch...",
  "timestamp": "2026-04-17T10:03:00.000Z",
  "projectId": "cl_proj_abc123",
  "metadata": { "albumId": "alb_12" },
  "data": {
    "jobId": "cm_mod_batch789",
    "projectId": "cl_proj_abc123",
    "status": "COMPLETED",
    "inputUrl": "https://cdn.example.com/a.jpg",
    "inputUrls": [
      "https://cdn.example.com/a.jpg",
      "https://cdn.example.com/b.jpg"
    ],
    "inputType": "image",
    "itemCount": 2,
    "safe": false,
    "violations": [
      { "category": "violence", "confidence": 0.51, "severity": "high" }
    ],
    "scores": { /* max score per category across all images */ },
    "flaggedFrames": [
      { "frameIndex": 1, "violations": [
        { "category": "violence", "confidence": 0.51, "severity": "high" }
      ]}
    ],
    "framesAnalyzed": 2,
    "processingTimeMs": 680,
    "items": [
      {
        "index": 0,
        "inputUrl": "https://cdn.example.com/a.jpg",
        "safe": true,
        "violations": [],
        "scores": {},
        "flaggedFrames": [],
        "framesAnalyzed": 1,
        "processingTimeMs": 320
      },
      {
        "index": 1,
        "inputUrl": "https://cdn.example.com/b.jpg",
        "safe": false,
        "violations": [
          { "category": "violence", "confidence": 0.51, "severity": "high" }
        ],
        "scores": { "violence": 0.51 },
        "flaggedFrames": [
          { "frameIndex": 0, "violations": [
            { "category": "violence", "confidence": 0.51, "severity": "high" }
          ]}
        ],
        "framesAnalyzed": 1,
        "processingTimeMs": 360
      }
    ],
    "metadata": { "albumId": "alb_12", "batchSize": 2 },
    "createdAt": "2026-04-17T10:02:58.000Z",
    "completedAt": "2026-04-17T10:03:00.000Z"
  }
}

Payload: moderation.failed

Fires when the worker cannot download or analyze the input (unreachable URL, unsupported format, timeout, etc.). The HTTP response to your create call also returns a structured error with the same errorMessage.

moderation.failed
{
  "event": "moderation.failed",
  "eventId": "cm9a8w2fail...",
  "timestamp": "2026-04-17T10:02:05.000Z",
  "projectId": "cl_proj_abc123",
  "metadata": { "userId": "u_42" },
  "data": {
    "jobId": "cm_mod_err123",
    "projectId": "cl_proj_abc123",
    "status": "FAILED",
    "inputUrl": "https://source.example.com/private.mp4",
    "inputType": "video",
    "safe": true,
    "framesAnalyzed": 0,
    "errorMessage": "HTTP 403 fetching input URL",
    "metadata": { "userId": "u_42" },
    "createdAt": "2026-04-17T10:02:03.000Z",
    "completedAt": "2026-04-17T10:02:05.000Z"
  }
}

Payload: video.conversion.failed

video.conversion.failed
{
  "event": "video.conversion.failed",
  "eventId": "cm9a8w3err...",
  "timestamp": "2026-04-17T10:00:31.000Z",
  "projectId": "cl_proj_abc123",
  "metadata": { "userId": "u_42" },
  "data": {
    "jobId": "cm_job_xyz789",
    "projectId": "cl_proj_abc123",
    "status": "FAILED",
    "inputUrl": "https://source.example.com/broken.mp4",
    "errorCode": "DOWNLOAD_FAILED",
    "errorMessage": "HTTP 403 fetching input URL after 3 retries",
    "metadata": { "userId": "u_42" },
    "createdAt": "2026-04-17T09:58:12.000Z",
    "failedAt":  "2026-04-17T10:00:31.000Z"
  }
}

Request Headers

Every webhook POST includes the following headers:

Content-Type
header

application/json — payload is always JSON.

User-Agent
header

BaxCloud-VideoConverter/1.0 for video.conversion.* (HLS worker pipeline), or BaxCloud-Webhooks/1.0 for standalone AI jobs (categorization.*, moderation.*).

X-BaxCloud-Event
header

The event type (e.g. moderation.completed). Useful when you route a single endpoint to many handlers.

X-BaxCloud-Signature
header

Present when the webhook has a secret. HMAC-SHA256 of the raw body as lowercase hex. Video conversion events prefix the digest with sha256=; standalone AI events send the hex digest only. See below.

Verifying the Signature

The signature is always HMAC-SHA256(secret, raw_body) as lowercase hex. Video conversion webhooks send sha256=<hex>; standalone moderation and categorization webhooks send the hex digest without a prefix. Always compute over the exact raw bytes we sent — re-serializing the parsed JSON will produce a different signature. Use a constant-time comparison to avoid timing attacks.

curl (manual verify)
# Compute expected digest (hex only)
echo -n "$RAW_BODY" \
  | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex \
  | awk '{print $2}'

# Video conversion: compare against X-BaxCloud-Signature after stripping "sha256="
# Standalone AI (moderation/categorization): compare hex digest directly

Use the SDK helper if you can — it does raw-body capture, header parsing and constant-time comparison for you. It expects the hex digest without the sha256= prefix, which matches standalone moderation.* and categorization.* webhooks. For video.conversion.*, strip the prefix before calling the SDK or use the manual verify snippet above.

import { webhookMiddleware } from '@baxcloud/baxcloud-server-sdk';
app.post('/webhooks/baxstream',
  express.json({
    verify: (req: any, _res, buf) => { req.rawBody = buf.toString('utf8'); },
  }),
  webhookMiddleware(process.env.BAXCLOUD_WEBHOOK_SECRET!),
  (req, res) => {
    const ev = req.baxcloudEvent; // verified + parsed
    res.json({ ok: true });
  },
);

Delivery & Retries

PropertyValue
MethodPOST with JSON body, no query string
Timeout10 seconds. Endpoints that do heavy work synchronously will time out — push to a queue and ack fast.
SuccessAny HTTP 2xx response counts as delivered.
FailureNon-2xx, network errors and timeouts are logged in Dashboard → Webhooks → Deliveries with status code and latency.
OrderingBest-effort. Use data.completedAt / timestamp if you need to order events server-side.
IdempotencyTreat delivery as at-least-once. Track processed eventId values to dedupe.
SubscriptionsEach webhook endpoint chooses which events to receive. Multiple endpoints per project are supported.

Best-practice receiver checklist

  • Verify X-BaxCloud-Signature on every request, reject mismatches with 401.
  • Capture the raw body for verification — never re-serialize JSON before checking the signature.
  • Return a 2xx response within 10 seconds; offload heavy work to a queue.
  • Dedupe by eventId so retries don't double-process.
  • Use X-BaxCloud-Event to fan out to per-event handlers without parsing the body first.
  • Log raw payloads (with secrets redacted) for at least 7 days while you build out integrations.

Node.js SDK

The official @baxcloud/baxstream package wraps all BaxStream API endpoints (video conversion, standalone categorization, standalone moderation). For webhook signature verification, also install @baxcloud/baxcloud-server-sdk (formerly @baxcloud/server-sdk).

terminal
npm install @baxcloud/baxstream

Initialize

init.ts
import { BaxCloudStreamClient } from '@baxcloud/baxstream';

const stream = new BaxCloudStreamClient({
  apiKey:    process.env.BAXCLOUD_API_KEY!,    // Dashboard → API Keys
  projectId: process.env.BAXCLOUD_PROJECT_ID!, // Dashboard → Projects → [Project]
});

Convert from URL

convert-url.ts
const job = await stream.createVideoConversionJob({
  inputUrl: 'https://example.com/video.mp4',
  outputFormats: ['360p', '720p'],
  ffmpegPreset: 'fast',       // optional
  hlsTime: 2,                 // optional, seconds per segment
  categorize: true,           // run AI categorization inline
  moderate:  true,            // run AI moderation inline
  metadata: { userId: 'u_42' },
  s3Config: {                  // optional if project defaults are set
    bucket: 'my-bucket',
    region: 'us-east-1',
    accessKey: 'AKIA...',
    secretKey: '...',
    cdnUrl: 'https://cdn.example.com',
  },
});

console.log(job.id, job.status); // "cm..." "PENDING"

Upload a File

upload.ts
import fs from 'node:fs';

const job = await stream.uploadVideoForConversion(
  fs.createReadStream('./my-video.mp4'),
  'my-video.mp4',
  { outputFormats: ['360p', '720p'], ffmpegPreset: 'fast' },
);

console.log(job.id, job.status);

Poll for Completion

poll.ts
let result = await stream.getVideoConversionJob(job.id);

while (result.status === 'PENDING' || result.status === 'PROCESSING') {
  await new Promise((r) => setTimeout(r, 5000)); // 5s poll
  result = await stream.getVideoConversionJob(job.id);
}

if (result.status === 'COMPLETED') {
  console.log('Master playlist:', result.outputUrl);
  console.log('Thumbnail:',       result.thumbnailUrl);
  // result.categorization + result.moderation are present when those features were enabled.
} else {
  console.error('Failed:', result.errorMessage);
}

List & Cancel

list-cancel.ts
// List jobs (filter + paginate)
const { items, total } = await stream.listVideoConversionJobs({
  status: 'PROCESSING',
  page: 1,
  pageSize: 20,
});

// Cancel a pending job
await stream.cancelVideoConversionJob('cm...');

AI Categorization

categorize.ts
// Standalone — no HLS conversion, no S3 required
const cat = await stream.createCategorizationJob({
  inputUrl: 'https://example.com/clip.mp4',
  inputType: 'video',   // or 'image'
  topK: 6,              // 1 primary + up to 5 others ≥ 1%
  maxFrames: 8,
});

console.log('Primary:', cat.primaryCategory, '(' + (cat.confidence * 100).toFixed(1) + '%)');
for (const c of cat.categories) {
  const marker = c.primary ? '★' : ' ';
  console.log(`${marker} ${c.name.padEnd(20)} ${(c.confidence * 100).toFixed(1)}%`);
}
// cat.scores is the FULL 20-category probability map for analytics/filtering.
// cat.tags is a short-tag gloss derived from the top frames.

Content Moderation

moderate.ts
// Standalone moderation (sync) — use the return value directly
const result = await stream.createModerationJob({
  inputUrl: 'https://example.com/photo.jpg',
  inputType: 'image',
  threshold: 0.30,
});

if (!result.safe) {
  for (const v of result.violations ?? []) {
    console.log(v.category, (v.confidence * 100).toFixed(1) + '%', `(${v.severity})`);
  }
}

// Batch images (also sync)
const batch = await stream.createModerationBatchJob({
  inputUrls: ['https://example.com/a.jpg', 'https://example.com/b.jpg'],
});
for (const item of batch.batchItems ?? []) {
  console.log(item.index, item.safe, item.violations);
}

// Inline with conversion — async; poll or use webhooks
const job = await stream.createVideoConversionJob({
  inputUrl: 'https://example.com/video.mp4',
  outputFormats: ['360p', '720p'],
  moderate: true,
});

// Re-fetch stored jobs later (optional)
const list = await stream.listModerationJobs({ status: 'COMPLETED' });
const one  = await stream.getModerationJob('cm...');

Receive & Verify Webhooks

The SDK ships a drop-in Express/Connect middleware that captures the raw body, verifies X-BaxCloud-Signature with constant-time comparison, and attaches the parsed event to req.baxcloudEvent. Standalone moderation webhooks send the hex digest directly (no sha256= prefix).

webhook-server.ts
import express from 'express';
import { webhookMiddleware } from '@baxcloud/baxcloud-server-sdk';

const app = express();

app.post(
  '/webhooks/baxstream',
  express.json({
    verify: (req: any, _res, buf) => {
      req.rawBody = buf.toString('utf8');
    },
  }),
  webhookMiddleware(process.env.BAXCLOUD_WEBHOOK_SECRET!),
  async (req, res) => {
    const ev = req.baxcloudEvent; // { event, eventId, timestamp, projectId, metadata, data }

    // eventId is globally unique — use it to dedupe at-least-once deliveries.
    if (await alreadyProcessed(ev.eventId)) return res.status(200).end();

    switch (ev.event) {
      case 'video.conversion.completed':
        await onVideoReady({
          jobId:        ev.data.jobId,
          outputUrl:    ev.data.outputUrl,
          thumbnailUrl: ev.data.thumbnailUrl,
          userId:       ev.metadata?.userId,
          categorization: ev.data.categorization, // undefined unless categorize=true
          moderation:     ev.data.moderation,     // undefined unless moderate=true
        });
        break;

      case 'moderation.started':
        // ev.data.jobId, ev.data.inputType ('video' | 'image')
        break;

      case 'moderation.completed':
        if (!ev.data.safe) {
          // ev.data.violations, ev.data.flaggedFrames
          // images: ev.data.framesAnalyzed === 1
          await flagForReview(ev.data);
        }
        break;

      case 'moderation.failed':
        await onModerationFailed(ev.data.jobId, ev.data.errorMessage);
        break;

      case 'video.conversion.failed':
        await onVideoFailed(ev.data.jobId, ev.data.errorMessage);
        break;
    }

    // ACK within 10s; push heavy work to a queue if needed.
    res.status(200).json({ ok: true });
  },
);

app.listen(3000);

Job Statuses

StatusDescription
PENDINGJob queued, waiting for a worker.
PROCESSINGWorker is downloading, probing, and transcoding the video.
COMPLETEDAll renditions generated and uploaded to S3. Output URLs available.
FAILEDConversion failed after all retry attempts. Check errorMessage.
CANCELLEDJob was cancelled before processing started.

Output File Structure

After conversion, the following files are uploaded to your S3 bucket under the configured prefix:

S3 bucket structure
{prefix}/
├── master.m3u8            # Master playlist (adaptive bitrate switching)
├── thumbnail.jpg          # Auto-generated thumbnail
├── 360p/
│   ├── playlist.m3u8      # 360p variant playlist
│   ├── segment_000.ts     # HLS segments
│   ├── segment_001.ts
│   └── ...
├── 480p/
│   ├── playlist.m3u8
│   └── ...
└── 720p/
    ├── playlist.m3u8
    └── ...