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.30. 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.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
| 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": { "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
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.30. 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.30,
"metadata": { "userId": "u_42" }
}'Moderate Image
https://api.baxcloud.tech/moderate/image — synchronous. The HTTP response returns status: "COMPLETED" with safe, violations, and related fields. Webhooks are optional.inputUrlURL of the image to moderate.
thresholdScore threshold for flagging (0–1). Default: 0.30. 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
}'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
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).curlcurl -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)
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[].curlcurl -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
https://api.baxcloud.tech/moderate/images/upload — multipart field images (repeat for each file, 1–20 images, max 20 MB each).curlcurl -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:
inputUrlmust be publicly reachable (HTTP 200). Extensionless URLs (e.g. signed CDN links) work when you setinputType: "image"or usePOST /moderate/image— the worker also sniffsContent-Typeand magic bytes after download. - Image responses:
framesAnalyzedis1per image;flaggedFramesusesframeIndex: 0for single-image jobs. Batch jobs includeitems[]with one entry per image.
Sync API vs webhooks
| Flow | Execution | How to get results |
|---|---|---|
| Standalone categorization & moderation | Synchronous | HTTP response (status: COMPLETED). Webhooks optional. |
| HLS video conversion | Async worker | Poll GET …/jobs/:id or video.conversion.* webhooks |
Conversion + inline moderate | Async | video.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"
}| Code | HTTP | Message | What to do |
|---|---|---|---|
| BAXSTREAM_MODERATION_PLAN_NOT_INCLUDED | 403 | Your plan does not include AI content moderation. | Upgrade your subscription (details.helpUrl → Billing). |
| BAXSTREAM_MODERATION_INPUT_INVALID | 400 | Could 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_UNREACHABLE | 502 | Could not download inputUrl. | Ensure the URL is publicly reachable and returns HTTP 200. |
| BAXSTREAM_MODERATION_WORKER_UNAVAILABLE | 503 | AI analysis service is temporarily unavailable. | Retry after a short delay. |
| BAXSTREAM_MODERATION_INVALID_IMAGE_TYPE | 400 | Uploaded file is not a supported image type. | Send JPEG, PNG, GIF, WebP, BMP, or TIFF via multipart field "image". |
| BAXSTREAM_MODERATION_NO_FILE | 400 | No image file uploaded. | POST multipart/form-data with field name "image". |
| BAXSTREAM_MODERATION_FAILED | 400 | Moderation job failed. | Check inputUrl, plan limits, and retry. See docs for details.helpUrl. |
| BAXSTREAM_PROJECT_INACTIVE | 403 | Project is not active. | Reactivate the project in the dashboard. |
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 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).
| 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, inputType, status, createdAt; batch jobs add inputUrls, itemCount |
moderation.completed | Moderation finished | inputType, aggregate safe, violations[], flaggedFrames, scores, framesAnalyzed, processingTimeMs; image jobs also include items[] (per-image results). Batch jobs add inputUrls, itemCount. |
moderation.failed | Moderation failed | status (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).
| Field | Type | Present on |
|---|---|---|
jobId | string | all events |
projectId | string | all events |
status | PENDING | COMPLETED | FAILED | all events |
inputUrl | string | all events |
inputType | "video" | "image" | all events |
safe | boolean | *.completed only |
violations | [{ category, confidence, severity }] | *.completed (empty when safe) |
scores | { category: number } (13 categories) | *.completed only |
flaggedFrames | [{ frameIndex, violations[] }] | *.completed (empty when safe) |
framesAnalyzed | integer | *.completed — 1 for images, up to maxFrames for video |
processingTimeMs | integer | *.completed only |
errorMessage | string | *.failed only |
metadata | object | null | all events (also hoisted to envelope root) |
createdAt | ISO-8601 | all events |
completedAt | ISO-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-Typeapplication/json — payload is always JSON.
User-AgentBaxCloud-VideoConverter/1.0 for video.conversion.* (HLS worker pipeline), or BaxCloud-Webhooks/1.0 for standalone AI jobs (categorization.*, moderation.*).
X-BaxCloud-EventThe event type (e.g. moderation.completed). Useful when you route a single endpoint to many handlers.
X-BaxCloud-SignaturePresent 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 directlyUse 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
| 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/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).
terminalnpm install @baxcloud/baxstreamInitialize
init.tsimport { 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.tsconst 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.tsimport 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.tslet 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.tsimport 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
| 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
└── ...