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.25. 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.25,
    "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": { /* ... */ }
  }'

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.

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.25. 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.25,
    "metadata": { "userId": "u_42" }
  }'

Moderate Image

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

inputUrl
string
required

URL of the image to moderate.

threshold
number

Score threshold for flagging (0–1). Default: 0.25. 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
  }'

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 are how BaxStream pushes job results to your backend in real time, so you don't have to poll. Configure one or more endpoints in Dashboard → Webhooks, subscribe to the events you care about, 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 9 events across 3 lifecycles. *.started events fire as soon as a worker picks up the job, *.completed on success, and *.failed on terminal errors.

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
moderation.completedModeration finishedsafe, violations[], per-frame flaggedFrames, full scores map
moderation.failedModeration failederrorMessage

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 9 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
    }
  }
}

Payload: moderation.completed (flagged)

moderation.completed
{
  "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",
    "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 */ },
    "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" },
    "completedAt": "2026-04-17T10:01:42.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 — identify our worker in your logs.

X-BaxCloud-Event
header

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

X-BaxCloud-Signature
header

Present when the webhook has a secret. Format: sha256=<hex_digest> — see signing recipe below.

Verifying the Signature

The signature is HMAC-SHA256(secret, raw_body) hex-encoded, prefixed with sha256=. Always compute it 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)
# 1. The webhook arrives — capture body + headers
echo -n "$RAW_BODY" \
  | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex \
  | awk '{print "sha256="$2}'
# Compare the output against the value of X-BaxCloud-Signature
# If they match, the request is authentic.

Use the SDK helper if you can — it does raw-body capture, header parsing and constant-time comparison for you:

import { webhookMiddleware } from '@baxcloud/server-sdk';
app.post('/webhooks/baxstream',
  express.raw({ type: 'application/json' }),
  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/server-sdk wraps all BaxStream endpoints (video conversion, standalone categorization, standalone moderation) plus webhook signature verification. Zero runtime dependencies, full TypeScript types.

terminal
npm install @baxcloud/server-sdk

Initialize

init.ts
import { BaxCloudClient } from '@baxcloud/server-sdk';

// API key + Project ID are required — base URL defaults to https://api.baxcloud.tech
const client = new BaxCloudClient({
  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 client.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 client.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 client.getVideoConversionJob(job.id);

while (result.status === 'PENDING' || result.status === 'PROCESSING') {
  await new Promise((r) => setTimeout(r, 5000)); // 5s poll
  result = await client.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 client.listVideoConversionJobs({
  status: 'PROCESSING',
  page: 1,
  pageSize: 20,
});

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

AI Categorization

categorize.ts
// Standalone — no HLS conversion, no S3 required
const cat = await client.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
const mod = await client.createModerationJob({
  inputUrl: 'https://example.com/clip.mp4',
  inputType: 'video',
  threshold: 0.25,     // default
});

if (!mod.safe) {
  // Each violation has { category, confidence, severity: 'low'|'medium'|'high'|'critical' }
  for (const v of mod.violations) {
    console.log(v.category, (v.confidence * 100).toFixed(1) + '%', `(${v.severity})`);
  }
  // mod.flaggedFrames pinpoints which frames triggered which categories.
}

// Inline with conversion — fires both 'video.conversion.completed' AND 'moderation.completed'
const job = await client.createVideoConversionJob({
  inputUrl: 'https://example.com/video.mp4',
  outputFormats: ['360p', '720p'],
  moderate: true,
});

// List & retrieve stored moderation jobs
const list = await client.listModerationJobs({ status: 'COMPLETED' });
const one  = await client.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.

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

const app = express();

app.post(
  '/webhooks/baxstream',
  express.raw({ type: 'application/json' }),
  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.completed':
        if (!ev.data.safe) await flagForReview(ev.data);
        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
    └── ...