CLOUDFLARE STREAM
Serverless video platform for on-demand and live video. Upload, encode, store, and deliver globally via a single REST API — no infrastructure to manage.
LIVE REST API HLS / DASH RTMPS / SRT Workers AI MCP Ready
npx skills add imbilawork/2nth-skills@cloudflare-stream
01 — OVERVIEW

WHAT IS CLOUDFLARE STREAM

Cloudflare Stream is a fully managed video platform — upload a video file, get back an encoding pipeline, global CDN delivery, an embeddable player, and a playback API. It handles on-demand VOD and real-time live streaming in the same product.

For AI agents, Stream is the right tool whenever a workflow involves video: storing customer walkthroughs, live-streaming product launches, transcribing support calls, or building AI-powered media pipelines on Cloudflare Workers.

Base URL
api.cloudflare.com/client/v4
Auth Header
Authorization: Bearer {token}
Max File Size
30 GB
Max FPS
70 FPS
Resolutions
360p → 1080p
Live Protocols
RTMPS · SRT
Delivery
HLS · DASH · MP4
Codec
H.264 + AAC

Supported Upload Formats

MP4, MKV, MOV, AVI, FLV, MPEG-2 TS, MPEG-2 PS, MXF, LXF, GXF, 3GP, WebM, MPG, QuickTime. Recommended: MP4 container · H.264 video · AAC audio · ≤60 FPS.

02 — ARCHITECTURE

HOW STREAM FITS IN

Stream sits between your application and your viewers. You push video in via the API; Stream encodes, stores, and delivers it. Your Workers or AI agents talk to the API to manage assets and generate secure playback URLs.

Source Video
file / URL / webcam / encoder
Stream Ingest
Upload API / RTMPS / SRT
Encode
H.264 · ABR · 360-1080p
Store
Cloudflare edge PoPs
Deliver
HLS / DASH / MP4
Webhooks fire at each state transition · Workers AI runs post-encode
Your Worker
API calls · signed URLs
Stream API
manage assets · live inputs
AI Agent
transcribe · caption · search
Viewer
<stream> · HLS player
03 — UPLOAD METHODS

GETTING VIDEO INTO STREAM

Four distinct upload paths depending on where the video originates and who is uploading it.

Method 1 — Direct Upload (server-side)

POST a multipart form or a tus resumable upload from your backend. Ideal for server-initiated workflows — processing pipelines, content ingestion, automated archiving.

# Upload via multipart form (small files < 200 MB)
POST https://api.cloudflare.com/client/v4/accounts/{account_id}/stream
Authorization: Bearer {token}
Content-Type: multipart/form-data

--boundary
Content-Disposition: form-data; name="file"; filename="video.mp4"
Content-Type: video/mp4
[binary data]
--boundary--

# Response includes uid, status, thumbnail, playback URLs
{
  "result": {
    "uid": "ea95132c15732419d526b5cae4c20c56",
    "status": { "state": "queued" },
    "thumbnail": "https://videodelivery.net/ea95.../thumbnails/thumbnail.jpg",
    "playback": {
      "hls": "https://videodelivery.net/ea95.../manifest/video.m3u8",
      "dash": "https://videodelivery.net/ea95.../manifest/video.mpd"
    }
  }
}

Method 2 — Upload by URL (pull from external source)

Pass a publicly accessible URL and Stream fetches and encodes it asynchronously. Useful when pulling from S3, a CMS asset library, or a third-party CDN.

POST https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/copy
Authorization: Bearer {token}
Content-Type: application/json

{
  "url": "https://your-bucket.s3.amazonaws.com/source/video.mp4",
  "meta": {
    "name": "Product Walkthrough Q1 2026"
  },
  "requireSignedURLs": false,
  "allowedOrigins": ["yourdomain.com"]
}

Method 3 — Direct Creator Uploads (client-side, one-time URL)

Your backend mints a single-use upload URL and passes it to the browser. The user uploads directly to Cloudflare — your API token never touches the client. This is the correct pattern for user-generated content platforms.

# Step 1: Backend mints the upload URL
POST https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/direct_upload
Authorization: Bearer {token}
Content-Type: application/json

{
  "maxDurationSeconds": 3600,
  "expiry": "2026-04-01T00:00:00Z",
  "meta": { "uploadedBy": "user_123" },
  "requireSignedURLs": true
}

# Response
{
  "result": {
    "uid": "2f01...",
    "uploadURL": "https://upload.videodelivery.net/2f01..."
  }
}

# Step 2: Client POSTs directly to uploadURL (tus protocol)
# No auth header needed — URL is the credential
tus Resumable Uploads

For files over 200 MB use the tus protocol (tus-js-client or uppy). Set endpoint to https://api.cloudflare.com/client/v4/accounts/{account_id}/stream with the Authorization header as tus metadata.

Upload API Fields Reference

FieldTypeDescription
uidstringUnique video identifier — used in all playback URLs
meta.namestringHuman-readable label for the video
requireSignedURLsboolRestrict playback to signed tokens only
allowedOriginsstring[]CORS-style origin whitelist for the player
scheduledDeletiondatetimeAuto-delete at this UTC timestamp
maxDurationSecondsintHard cap on video length (creator uploads)
status.statestringqueuedinprogressready / error

Video Management

# List all videos
GET /accounts/{account_id}/stream?limit=50&status=ready&search=walkthrough

# Get single video
GET /accounts/{account_id}/stream/{video_uid}

# Update metadata
POST /accounts/{account_id}/stream/{video_uid}
{ "meta": { "name": "Updated Title" }, "requireSignedURLs": true }

# Delete
DELETE /accounts/{account_id}/stream/{video_uid}
04 — LIVE STREAMING

STREAM LIVE

Stream Live accepts RTMPS and SRT ingest. Every live broadcast is automatically recorded — the VOD asset becomes available within 60 seconds of the stream ending.

Create a Live Input

POST https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/live_inputs
Authorization: Bearer {token}
Content-Type: application/json

{
  "meta": { "name": "Weekly Webinar — Product Updates" },
  "recording": {
    "mode": "automatic",
    "timeoutSeconds": 300
  }
}

# Response — store rtmps.url + rtmps.streamKey for your encoder
{
  "result": {
    "uid": "66c...",
    "rtmps": {
      "url": "rtmps://live.cloudflare.com:443/live/",
      "streamKey": "66c...abc"
    },
    "srt": {
      "url": "srt://live.cloudflare.com:778",
      "streamId": "66c...",
      "passphrase": "xyz..."
    },
    "playback": {
      "hls": "https://videodelivery.net/66c.../manifest/video.m3u8",
      "dash": "https://videodelivery.net/66c.../manifest/video.mpd"
    }
  }
}

Live Input Management

# List live inputs
GET /accounts/{account_id}/stream/live_inputs

# Get live input + all recorded VODs
GET /accounts/{account_id}/stream/live_inputs/{live_input_uid}/videos

# Delete live input
DELETE /accounts/{account_id}/stream/live_inputs/{live_input_uid}

OBS / FFmpeg Setup

FieldOBS Value
ServiceCustom
Serverrtmps://live.cloudflare.com:443/live/
Stream Key{streamKey} from live input response
Encoderx264 · CBR · 6000 kbps max · Keyframe 2s
# FFmpeg RTMPS ingest (requires automatic reconnect flags)
ffmpeg -re -i input.mp4 \
  -c:v libx264 -preset veryfast -b:v 4000k -maxrate 4000k -bufsize 8000k \
  -g 60 -keyint_min 60 \
  -c:a aac -b:a 128k \
  -f flv "rtmps://live.cloudflare.com:443/live/{streamKey}"
Reconnect Handling

OBS reconnects automatically. FFmpeg does not — wrap in a shell loop or use -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 30 flags. Cloudflare holds the stream open for timeoutSeconds after the last packet before marking it ended.

Adaptive Bitrate Behaviour

Stream Live calculates ABR renditions from the actual ingest bitrate — not a fixed ladder. Low-motion content (slides, talking heads) will produce fewer/lower renditions. High-motion content (sports, gaming) triggers higher estimates. Plan encoder bitrate accordingly.

05 — PLAYBACK & PLAYER

SERVING VIDEO TO VIEWERS

Stream Player (Embed)

The fastest path. Cloudflare's own player handles adaptive streaming, captions, thumbnails, and analytics out of the box.

<!-- Load the SDK (once per page) -->
<script src="https://embed.cloudflarestream.com/embed/sdk.latest.js"></script>

<!-- Embed the player -->
<stream
  src="ea95132c15732419d526b5cae4c20c56"
  controls
  autoplay
  muted
  loop
  width="100%"
  poster="https://videodelivery.net/ea95.../thumbnails/thumbnail.jpg?time=5s&height=360"
></stream>

<!-- JavaScript API -->
<script>
  const player = Stream(document.querySelector('stream'));
  player.addEventListener('play', () => console.log('playing'));
  player.play();
  player.pause();
  player.currentTime = 30;
  player.volume = 0.5;
</script>

HLS / DASH — Custom Player

Use any player (Video.js, Shaka Player, hls.js) by pointing it at the manifest URL.

# HLS manifest
https://videodelivery.net/{video_uid}/manifest/video.m3u8

# DASH manifest
https://videodelivery.net/{video_uid}/manifest/video.mpd

# Direct MP4 (for download or simple playback)
https://videodelivery.net/{video_uid}/downloads/default.mp4

# Example with hls.js
const hls = new Hls();
hls.loadSource('https://videodelivery.net/{video_uid}/manifest/video.m3u8');
hls.attachMedia(videoElement);

Thumbnails

# Default thumbnail (at 0:00)
https://videodelivery.net/{video_uid}/thumbnails/thumbnail.jpg

# Thumbnail at specific time
https://videodelivery.net/{video_uid}/thumbnails/thumbnail.jpg?time=15s

# Resize
https://videodelivery.net/{video_uid}/thumbnails/thumbnail.jpg?height=360&width=640&fit=crop

# Animated GIF preview (first 3 seconds by default)
https://videodelivery.net/{video_uid}/thumbnails/thumbnail.gif?duration=5s
06 — ACCESS CONTROL

SIGNED URLS & RESTRICTIONS

When requireSignedURLs: true is set on a video, all direct playback URLs return 401. Viewers must present a signed token issued by your backend.

Step 1 — Enable on a Video

POST /accounts/{account_id}/stream/{video_uid}
{ "requireSignedURLs": true }

Step 2 — Generate a Signing Key

POST /accounts/{account_id}/stream/keys
# Returns a pemKey (RSA private key) — store securely, never expose to clients
{
  "result": {
    "id": "key_abc123",
    "pemKey": "-----BEGIN RSA PRIVATE KEY-----\n..."
  }
}

Step 3 — Issue Tokens in Your Worker

// Cloudflare Worker — sign a playback token
import { SignJWT, importPKCS8 } from 'jose';

export async function signStreamToken(videoUID, pemKey, keyId, expiresIn = '1h') {
  const privateKey = await importPKCS8(pemKey, 'RS256');
  return new SignJWT({})
    .setProtectedHeader({ alg: 'RS256', kid: keyId })
    .setSubject(videoUID)
    .setIssuedAt()
    .setExpirationTime(expiresIn)
    .sign(privateKey);
}

// Usage — append token to any Stream URL
const token = await signStreamToken(uid, pemKey, keyId, '2h');
const signedHLS = `https://videodelivery.net/${token}/manifest/video.m3u8`;
const signedEmbed = `<stream src="${token}" controls></stream>`;
Token Claims

JWT sub must be the video UID. Optional claims: accessRules to restrict by IP range, country, or any custom claim in the viewer's JWT. Tokens are stateless — no database lookups at playback time.

Origin Restrictions (No Auth Required)

# Restrict playback to specific domains without signed URLs
POST /accounts/{account_id}/stream/{video_uid}
{ "allowedOrigins": ["yourdomain.com", "app.yourdomain.com"] }
07 — WEBHOOKS

STREAM EVENTS

Subscribe to Stream webhook events to trigger downstream workflows — transcription jobs, CMS updates, notifications, or AI processing pipelines.

Register a Webhook Endpoint

PUT /accounts/{account_id}/stream/webhook
Authorization: Bearer {token}
Content-Type: application/json

{
  "notificationUrl": "https://your-worker.workers.dev/stream-webhook"
}

Event Types

EventWhen It Fires
stream.video.readyVideo finished encoding and is ready for playback
stream.video.erroredEncoding failed — check status.errReasonCode
stream.live_input.connectedRTMPS/SRT encoder connected and stream is live
stream.live_input.disconnectedEncoder disconnected; recording will save after timeout
stream.live_input.recording_startedRecording is being written to storage
stream.live_input.recording_readyPost-stream VOD is ready and can be played back

Webhook Payload & Verification

// Worker receiving a webhook
export default {
  async fetch(request) {
    const body = await request.json();
    const { uid, status, meta } = body.result;

    // Cloudflare signs the request — verify with HMAC-SHA256
    const signature = request.headers.get('Webhook-Signature');
    // signature = 'time=1234;sig1=...'

    if (status.state === 'ready') {
      // Trigger downstream: transcription, CMS update, AI summary
      await triggerTranscription(uid);
    }

    return new Response('OK');
  }
};
08 — WORKERS AI INTEGRATION

AI-POWERED VIDEO WORKFLOWS

Combine Stream with Workers AI to build intelligent video pipelines without leaving Cloudflare's network. Common patterns: auto-transcription, caption generation, content moderation, and semantic video search.

Auto-Transcription on Ready

// workers/stream-pipeline.js
// Triggered by stream.video.ready webhook

export default {
  async fetch(request, env) {
    const event = await request.json();
    const { uid } = event.result;

    // 1. Download the audio track from Stream
    const audioResp = await fetch(
      `https://videodelivery.net/${uid}/downloads/default.mp3`
    );
    const audioBuffer = await audioResp.arrayBuffer();

    // 2. Transcribe with Whisper via Workers AI
    const transcription = await env.AI.run('@cf/openai/whisper', {
      audio: [...new Uint8Array(audioBuffer)],
    });

    // 3. Store transcript (D1, KV, R2...)
    await env.DB.prepare(
      'INSERT INTO transcripts (video_uid, text, language) VALUES (?, ?, ?)'
    ).bind(uid, transcription.text, transcription.language).run();

    // 4. Generate VTT captions and attach to video
    const vtt = buildVTT(transcription.words);
    await uploadCaptions(uid, vtt, env.CF_API_TOKEN, env.CF_ACCOUNT_ID);

    return new Response('Pipeline complete');
  }
};

Upload VTT Captions to Stream

async function uploadCaptions(videoUID, vttContent, token, accountId) {
  const formData = new FormData();
  formData.append('file', new Blob([vttContent], { type: 'text/vtt' }), 'captions.vtt');

  return fetch(
    `https://api.cloudflare.com/client/v4/accounts/${accountId}/stream/${videoUID}/captions/en`,
    {
      method: 'PUT',
      headers: { Authorization: `Bearer ${token}` },
      body: formData,
    }
  );
}

AI-Powered Summary Generation

// After transcription, generate a structured summary
const summary = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
  messages: [
    {
      role: 'system',
      content: 'You are a video content analyst. Extract key topics, action items, and a brief summary from the transcript.'
    },
    {
      role: 'user',
      content: `Transcript:\n${transcription.text}`
    }
  ]
});

// Store alongside the video in your CMS or D1 database
Full Pipeline Architecture

Stream webhook → Worker receives stream.video.ready → Download audio → Whisper transcription → LLaMA summary → Store in D1 → Attach VTT captions back to video → Update search index → Notify user. All within Cloudflare's network, zero egress fees.

Typical AI Workflow Patterns

PatternModelsOutput
Auto-transcription@cf/openai/whisperFull transcript with word timestamps
Auto-captioningWhisper → VTT formatterVTT file attached to video
Meeting summaryWhisper + LLaMA 3.1Structured JSON: topics, action items
Content moderationCustom classifierFlag/approve workflow
Semantic searchWhisper + embeddings + VectorizeNatural language video search
Chapter generationWhisper + LLaMATimestamped chapter markers
09 — MCP TOOLS

AI AGENT TOOL SET

When an AI agent has the Cloudflare Stream skill installed, it has access to the following tool definitions for managing videos programmatically.

stream_upload_url
Upload a video by pulling from a public URL
stream_create_upload
Mint a one-time direct creator upload URL
stream_list_videos
List videos with filter by status, search term
stream_get_video
Fetch a single video's metadata and playback URLs
stream_update_video
Update meta, requireSignedURLs, allowedOrigins
stream_delete_video
Permanently delete a video and its storage
stream_create_live_input
Create live input, return RTMPS/SRT credentials
stream_list_live_inputs
List all live inputs and their current status
stream_sign_token
Issue a signed JWT playback token for a video
stream_upload_captions
Attach a VTT caption file to a video by language code
stream_transcribe
Run Whisper transcription via Workers AI on a video
stream_get_embed
Return ready-to-paste <stream> embed HTML

Example Agent Conversations

User
Upload the video at https://assets.acme.com/webinar-march.mp4, restrict it to signed URLs only, and give me the embed code.
Agent
I'll use stream_upload_url to pull the video into Stream, then stream_update_video to enable requireSignedURLs, generate a 24-hour signed token with stream_sign_token, and return the embed. One moment while it encodes — I'll poll status.state until it hits ready.
User
Set up a live stream for our Friday product demo and give me the OBS settings.
Agent
Creating the live input now with stream_create_live_input. For OBS: Service → Custom, Server → rtmps://live.cloudflare.com:443/live/, Stream Key → [key]. Use x264, CBR, 4000 kbps, 60fps, 2-second keyframe interval. The HLS playback URL for your site is ready immediately — the stream goes live as soon as OBS connects.
User
Transcribe all videos uploaded this week and add English captions.
Agent
I'll use stream_list_videos filtered by creation date, then run stream_transcribe on each via Workers AI (Whisper), format the output as VTT, and call stream_upload_captions with language code en. I'll batch these to avoid rate limits — estimated 3-5 minutes for the full set.
10 — PRICING

STREAM COSTS

Stream pricing is based on minutes stored and minutes delivered. There is no charge for encoding, transcoding, or packaging.

DimensionCostNotes
Storage (minutes)$5 / 1,000 min storedMeasured at encoded output duration
Delivery (minutes)$1 / 1,000 min deliveredPer viewer-minute of playback
Live encodingIncludedNo extra charge for RTMPS/SRT ingest
Recording$5 / 1,000 min recordedLive streams auto-recorded
CaptionsIncludedVTT upload + delivery at no extra cost
Signed URLsIncludedNo charge for token issuance
Cost Planning Tip

For AI transcription pipelines: Workers AI Whisper costs apply separately (charged per audio second). Budget ~$0.005 per minute of audio. Schedule transcription jobs after stream.video.ready to avoid wasted calls on failed encodes.

11 — GOTCHAS

COMMON ISSUES & FIXES

IssueCauseFix
Video stuck in inprogress Large file still encoding Poll GET /stream/{uid} every 5s; hook stream.video.ready webhook instead of polling
Playback returns 401 requireSignedURLs is true but no token passed Pass signed JWT as the src in <stream> or append ?token=... to HLS URL
Live stream drops and doesn't reconnect FFmpeg doesn't auto-reconnect on RTMPS disconnect Add -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 30 to FFmpeg flags
Player not showing on page SDK script loaded after <stream> element Load sdk.latest.js in <head> or before the <stream> tag
ABR quality lower than expected Stream calculates renditions from ingest bitrate Increase encoder bitrate — Stream won't upscale. Target ≥6 Mbps for 1080p output
Thumbnail returns wrong frame Default is 0:00 which may be a black frame Set ?time=5s or specify thumbnailTimestampPct during upload
Creator upload URL expired expiry field set too short Set expiry to at least 30 minutes from now; mint a new URL if expired
Multi-channel audio mixed down Stream encodes to stereo by design Expected behaviour — pre-mix to stereo before upload if mix matters
12 — INSTALL

ADD THIS SKILL

Install the Cloudflare Stream skill into your AI agent or Claude Code project. Once installed, the agent has access to all API patterns, code examples, and MCP tool definitions on this page.

npx skills add imbilawork/2nth-skills@cloudflare-stream

Related Skills

Cloudflare Stream integrates naturally with these other 2nth.ai skills:

  • Shopify AI — embed product demo videos and live launch streams directly in Shopify storefronts
  • Sage X3 AI Integration — attach training videos to ERP onboarding workflows and knowledge bases
Reference Docs

Official Cloudflare Stream documentation: developers.cloudflare.com/stream