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_xxxxxxxxxxxxxxxxProject ID
Scopes jobs, billing, analytics
clxxxxxxxxxxxxxxS3 Bucket
Where converted files land
my-videos-bucketAuthentication
All requests to api.baxcloud.tech require two headers:
AuthorizationBearer YOUR_API_KEY — your BaxCloud API key with BaxStream permission enabled.
X-Project-IdYour project ID. All jobs are scoped to this project for billing and analytics.
example headersAuthorization: Bearer bax_xxxxxxxxxxxxxxxx
X-Project-Id: clxxxxxxxxxxxxxxQuick Start
Submit a conversion job with a single POST. Replace YOUR_API_KEY and YOUR_PROJECT_ID with your actual values.
curlcurl -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)
https://api.baxcloud.tech/video/convertRequest Body
inputUrlURL of the source video. Supported formats: MP4, MOV, AVI, MKV, WebM, FLV, WMV, M4V, TS, MTS, 3GP, OGV.
outputFormatsQuality presets to generate. Options: 360p, 480p, 720p, 1080p. Default: ["360p", "480p", "720p"].
ffmpegPresetEncoding speed/quality trade-off. Options: ultrafast, veryfast, fast (default), medium, slow, veryslow. Slower presets produce smaller files at the same quality.
hlsTimeHLS segment duration in seconds. Default: 2. Range: 1–10. Shorter segments = faster seek, slightly larger overhead.
s3ConfigS3 output configuration. Optional if project defaults are configured.
s3Config.bucketS3 bucket name.
s3Config.regionAWS region (e.g. us-east-1). Default: auto.
s3Config.endpointCustom S3 endpoint for S3-compatible storage (Cloudflare R2, MinIO, etc).
s3Config.prefixKey prefix for output files (e.g. videos/my-video/). Default: /.
s3Config.accessKeyS3 access key ID.
s3Config.secretKeyS3 secret access key.
s3Config.cdnUrlPublic / CDN base URL. When set, all output URLs (playlists, thumbnails) use this domain instead of the raw S3 URL. Keeps your S3 endpoint private.
categorizeAlso run AI video categorization alongside HLS conversion. Default: false. Results included in webhook payload.
moderateAlso run AI content moderation alongside HLS conversion. Default: false. Results included in webhook payload — you decide what to do with flagged content.
metadataArbitrary JSON metadata to attach to the job. Returned in webhooks and API responses.
Upload File (multipart)
https://api.baxcloud.tech/video/uploadUpload a video file directly instead of providing a URL. Max file size: 2 GB. Send as multipart/form-data.
curlcurl -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
videoThe video file to convert.
outputFormatsJSON array of quality presets, e.g. ["360p","720p"].
ffmpegPresetEncoding preset. Same options as the JSON API.
hlsTimeHLS segment duration (as string).
s3ConfigJSON object with S3 configuration. Same schema as the JSON API.
metadataJSON metadata object.
List Jobs
https://api.baxcloud.tech/video/jobsQuery Parameters
statusFilter by status: PENDING, PROCESSING, COMPLETED, FAILED, CANCELLED.
pagePage number (default: 1).
pageSizeItems per page (default: 20, max: 100).
Get Job
https://api.baxcloud.tech/video/jobs/:jobIdReturns the full job object including output URLs when completed.
curlcurl 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
https://api.baxcloud.tech/video/jobs/:jobIdOnly 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.
| Preset | Max Resolution | Video Bitrate | Audio Bitrate |
|---|---|---|---|
360p | 640 × 360 | 800 kbps | 96 kbps |
480p | 854 × 480 | 1400 kbps | 128 kbps |
720p | 1280 × 720 | 2800 kbps | 128 kbps |
1080p | 1920 × 1080 | 5000 kbps | 192 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
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.m3u8Content 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
https://api.baxcloud.tech/video/moderateinputUrlURL of the video or image to moderate.
inputTypevideo (default) or image.
maxFramesMax frames to extract from video (1–16). Default: 8.
thresholdScore threshold for flagging (0–1). Default: 0.25. Lower = more sensitive.
metadataArbitrary JSON metadata to attach to the job.
curlcurl -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
| Category | Severity | Description |
|---|---|---|
nudity_explicit | critical | Full nudity with visible genitals, hardcore pornographic imagery |
sexual_activity | critical | People engaged in sexual intercourse or explicit sexual acts |
suggestive_nudity | high | Topless, exposed buttocks, implied nudity from behind |
lingerie_underwear | medium | Lingerie, bra & panties, revealing underwear, boudoir shoots |
sexy_suggestive | low | Sensual dancing, seductive poses, tight/revealing clothing |
violence | high | Fighting, physical assault, armed conflict, war footage |
gore_graphic | critical | Extremely graphic violence, mutilation, corpses |
hate_symbols | critical | Hate group symbols, racist imagery, propaganda |
drugs_substances | medium | Drug use, paraphernalia, substance abuse |
weapons | medium | Firearms, knives, explosives displayed prominently |
child_safety | critical | Content endangering or exploiting minors |
self_harm | critical | Self-injury, suicidal imagery, pro-anorexia |
harassment | medium | Bullying, threatening, intimidating behavior |
List Moderation Jobs
https://api.baxcloud.tech/video/moderate/jobsGet Moderation Job
https://api.baxcloud.tech/video/moderate/jobs/:jobIdInline 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.
curlcurl -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
https://api.baxcloud.tech/categorize/videoinputUrlURL of the video to categorize.
metadataArbitrary JSON metadata to attach to the job.
curlcurl -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
https://api.baxcloud.tech/categorize/imageinputUrlURL of the image to categorize.
metadataArbitrary JSON metadata to attach to the job.
curlcurl -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
https://api.baxcloud.tech/moderate/videoinputUrlURL of the video to moderate.
maxFramesMax frames to extract from video (1–16). Default: 8.
thresholdScore threshold for flagging (0–1). Default: 0.25. Lower = more sensitive.
metadataArbitrary JSON metadata to attach to the job.
curlcurl -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
https://api.baxcloud.tech/moderate/imageinputUrlURL of the image to moderate.
thresholdScore threshold for flagging (0–1). Default: 0.25. Lower = more sensitive.
metadataArbitrary JSON metadata to attach to the job.
curlcurl -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:
| Method | Path | Description |
|---|---|---|
GET | /categorize/jobs | List categorization jobs (supports ?status=, ?page=, ?pageSize=) |
GET | /categorize/jobs/:jobId | Get a specific categorization job |
GET | /moderate/jobs | List moderation jobs |
GET | /moderate/jobs/:jobId | Get 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.
| Event | When it fires | Notable payload fields |
|---|---|---|
video.conversion.started | Worker began transcoding | jobId, inputUrl, outputFormats |
video.conversion.completed | All HLS renditions uploaded successfully | outputUrl, thumbnailUrl, renditionUrls, durationSec, costCents, plus categorization / moderation when those features are enabled |
video.conversion.failed | Conversion failed after retries | errorMessage, errorCode |
categorization.started | AI categorization began (standalone job) | jobId, inputUrl |
categorization.completed | Categorization finished | primaryCategory, categories[] (1 primary + others ≥1%), scores, tags |
categorization.failed | Categorization failed | errorMessage |
moderation.started | AI moderation began (standalone job) | jobId, inputUrl |
moderation.completed | Moderation finished | safe, violations[], per-frame flaggedFrames, full scores map |
moderation.failed | Moderation failed | 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 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-Typeapplication/json — payload is always JSON.
User-AgentBaxCloud-VideoConverter/1.0 — identify our worker in your logs.
X-BaxCloud-EventThe event type (e.g. video.conversion.completed). Useful when you route a single endpoint to many handlers.
X-BaxCloud-SignaturePresent 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
| Property | Value |
|---|---|
| Method | POST with JSON body, no query string |
| Timeout | 10 seconds. Endpoints that do heavy work synchronously will time out — push to a queue and ack fast. |
| Success | Any HTTP 2xx response counts as delivered. |
| Failure | Non-2xx, network errors and timeouts are logged in Dashboard → Webhooks → Deliveries with status code and latency. |
| Ordering | Best-effort. Use data.completedAt / timestamp if you need to order events server-side. |
| Idempotency | Treat delivery as at-least-once. Track processed eventId values to dedupe. |
| Subscriptions | Each webhook endpoint chooses which events to receive. Multiple endpoints per project are supported. |
Best-practice receiver checklist
- Verify
X-BaxCloud-Signatureon every request, reject mismatches with401. - Capture the raw body for verification — never re-serialize JSON before checking the signature.
- Return a
2xxresponse within 10 seconds; offload heavy work to a queue. - Dedupe by
eventIdso retries don't double-process. - Use
X-BaxCloud-Eventto 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.
terminalnpm install @baxcloud/server-sdkInitialize
init.tsimport { 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.tsconst 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.tsimport 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.tslet 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.tsimport 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
| Status | Description |
|---|---|
PENDING | Job queued, waiting for a worker. |
PROCESSING | Worker is downloading, probing, and transcoding the video. |
COMPLETED | All renditions generated and uploaded to S3. Output URLs available. |
FAILED | Conversion failed after all retry attempts. Check errorMessage. |
CANCELLED | Job 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
└── ...