Skip to content

Streams quickstart ​

In about twenty minutes you will create a live stream, give one customer access, open a viewing session from a browser and play a test pattern. Part 1 uses test keys and costs nothing. Part 2 uses live keys to show real video.

You need:

  • A tenant with Streams switched on. If a call returns 403 product_not_enabled, ask your Vabu contact to switch it on.
  • curl, Node 18 or later, and ffmpeg for part 2.
  • A secret key and a publishable key. See environments and keys.

Set your host and your test secret key:

sh
export API="https://api.vabu.live"
export SK="vk_test_..."

Part 1: build and check with test keys ​

A test key never reaches a real video network, so it never bills. Streams you create are simulated: they stay idle, nothing accepts a broadcast and the playback link does not play. Everything else is real, so you can build and check your whole flow.

1. Create a publishable key ​

The browser uses a publishable key. Create one with your secret key. The full key is shown once.

sh
curl -X POST "$API/v1/keys" \
  -H "Authorization: Bearer $SK" \
  -H "Idempotency-Key: quickstart-key-1" \
  -H "Content-Type: application/json" \
  -d '{ "type": "publishable", "name": "Quickstart page" }'

Copy key from the response into PK:

sh
export PK="vpk_test_..."

2. Create a live stream ​

sh
curl -X POST "$API/v1/streams/live_streams" \
  -H "Authorization: Bearer $SK" \
  -H "Idempotency-Key: quickstart-stream-1" \
  -H "Content-Type: application/json" \
  -d '{ "external_id": "quickstart-venue", "metadata": { "venue": "Kasarani" } }'
json
{
  "data": {
    "id": "ls_01J9ZQ4M8T3K6Y2E5W7N0B1C9D",
    "object": "live_stream",
    "provider": "simulated",
    "status": "idle",
    "latency_mode": "low",
    "reconnect_window": 60,
    "stream_key": "sim-...",
    "ingest": { "rtmp_url": "...", "srt_url": "..." },
    "playback_ids": [{ "id": "pb_...", "policy": "signed" }],
    "livemode": false
  }
}

Keep the id:

sh
export STREAM="ls_01J9ZQ4M8T3K6Y2E5W7N0B1C9D"

3. Give a customer access ​

Your own checkout would make this call after a payment. Use your own id for the customer. Here it is user_42.

sh
curl -X POST "$API/v1/access/entitlements" \
  -H "Authorization: Bearer $SK" \
  -H "Idempotency-Key: quickstart-grant-1" \
  -H "Content-Type: application/json" \
  -d '{
    "customer": "user_42",
    "resource": { "type": "ls", "id": "'"$STREAM"'" },
    "source": "tenant_grant",
    "source_ref": "quickstart-grant-1",
    "limits": { "max_devices": 2, "max_concurrent_sessions": 1, "concurrency_policy": "replace_oldest" }
  }'

You get 201 and an entitlement with "status": "active". The resource type for a live stream is ls, and for a video asset it is ast.

4. Create a customer token key ​

Your server signs a short token for each viewer. The key that signs it is made once:

sh
curl -X POST "$API/v1/access/customer_token_keys" \
  -H "Authorization: Bearer $SK" \
  -H "Content-Type: application/json" \
  -d '{ "name": "quickstart" }'

The response holds id (starts ctk_) and secret (starts cts_). The secret is shown once. Keep it on your server.

Sign a token for user_42. This needs no packages:

js
// sign-token.mjs
import { createHmac } from 'node:crypto'

const [keyId, secret, customer] = process.argv.slice(2)
const b64 = (value) => Buffer.from(JSON.stringify(value)).toString('base64url')
const now = Math.floor(Date.now() / 1000)

const head = b64({ alg: 'HS256', typ: 'JWT', kid: keyId })
const body = b64({ sub: customer, aud: 'vabu', iat: now, exp: now + 600 })
const signature = createHmac('sha256', secret).update(`${head}.${body}`).digest('base64url')

console.log(`${head}.${body}.${signature}`)
sh
export TOKEN=$(node sign-token.mjs ctk_... cts_... user_42)

The token lives for 10 minutes. Real apps sign one each time the page loads. See customer tokens for PHP, Python and Go.

5. Open a session from the browser ​

The browser sends the publishable key and the customer token. It never sees your secret key. Paste this into the console of any web page, after setting the three values:

js
const res = await fetch('https://api.vabu.live/v1/streams/playback_tokens', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Vabu-Publishable-Key': 'vpk_test_...',
    Authorization: 'Bearer <TOKEN>',
  },
  body: JSON.stringify({ resource: 'ls_...', device_id: 'browser-1' }),
})
console.log(await res.json())
json
{
  "data": {
    "id": "as_01J9ZQ7P2H5V8X1A4D6F9G3JKM",
    "object": "access_session",
    "customer": "user_42",
    "device_id": "browser-1",
    "state": "active",
    "heartbeat_interval": 30,
    "grant": {
      "sources": [{ "provider": "simulated", "url": "https://.../....m3u8?token=..." }],
      "failover": { "max_errors": 3, "window_seconds": 20 },
      "token_expires_at": "2026-10-12T20:11:30Z"
    }
  }
}

That is the whole viewer path: access checked, device counted, a playback link issued. Run it again with another device_id and, with replace_oldest, the first session ends at its next heartbeat. Remove the entitlement with DELETE /v1/access/entitlements/{id} and the next call is refused with 403 access_denied.

Part 2: see real video with live keys ​

Live keys use a real video network, so you can watch a picture. Stop ffmpeg when you are done.

6. Repeat steps 1 to 4 with live keys ​

Use your vk_live_... secret key. Test and live data never mix, so the publishable key, the stream, the entitlement and the token key all need to be made again, and the new publishable key starts with vpk_live_. This time the live stream comes with a real ingest address:

json
{
  "data": {
    "id": "ls_...",
    "status": "idle",
    "stream_key": "...",
    "ingest": {
      "rtmp_url": "rtmps://...",
      "rtmp_backup_url": "rtmp://...",
      "srt_url": "srt://...",
      "whip_url": null
    }
  }
}

7. Send a test pattern with ffmpeg ​

This command builds a moving test picture and a tone, encodes them the way a venue encoder would and sends them to your stream. Put the rtmp_url and the stream_key from step 6 in the two variables.

sh
RTMP_URL="rtmps://..."
STREAM_KEY="..."

ffmpeg -re \
  -f lavfi -i "testsrc2=size=1280x720:rate=30" \
  -f lavfi -i "sine=frequency=440:sample_rate=48000" \
  -c:v libx264 -preset veryfast -tune zerolatency -pix_fmt yuv420p \
  -b:v 2500k -maxrate 2500k -bufsize 5000k \
  -g 60 -keyint_min 60 -sc_threshold 0 \
  -c:a aac -b:a 128k -ar 48000 \
  -f flv "$RTMP_URL/$STREAM_KEY"

-g 60 at 30 frames a second gives a keyframe every 2 seconds, which is what the player needs. Leave it running.

Within about 30 seconds the stream moves from idle to connecting to active:

sh
curl "$API/v1/streams/live_streams/$STREAM" -H "Authorization: Bearer $SK"

You can also hear about it by webhook: streams.live_stream.active. See stream events.

8. Play it with hls.js ​

Save this as watch.html and serve it from your own site (any static server works). It opens a session, plays the link and keeps the session alive. The page gets its customer token from your own server: /customer-token is a route you write that signs a token for the signed-in viewer, as in step 4.

html
<!doctype html>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Watch</title>
<video id="video" controls playsinline style="width: 100%; max-width: 720px; background: #000"></video>
<p id="status">Starting...</p>

<script
  src="https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.5.17/hls.min.js"
  integrity="sha384-9v3HcdYrO3D+OPDTjZ40RXocgE4GtXVCd3/mCS62JsM93JXgI1afJVuwjFvsu6ni"
  crossorigin="anonymous"
></script>
<script>
  const API = 'https://api.vabu.live'
  const KEY = 'vpk_live_...'
  const STREAM = 'ls_...'

  const video = document.getElementById('video')
  const status = document.getElementById('status')
  const deviceId = localStorage.deviceId ?? (localStorage.deviceId = 'web-' + crypto.randomUUID())

  // Your own route: it signs a customer token for whoever is signed in.
  async function customerToken() {
    return (await fetch('/customer-token', { method: 'POST' })).text()
  }

  async function call(path, body = {}) {
    const res = await fetch(API + path, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Vabu-Publishable-Key': KEY,
        Authorization: 'Bearer ' + (await customerToken()),
      },
      body: JSON.stringify(body),
    })
    const json = await res.json()
    if (!res.ok) throw new Error(json.error.code + ': ' + json.error.message)
    return json.data
  }

  async function start() {
    const session = await call('/v1/streams/playback_tokens', { resource: STREAM, device_id: deviceId })
    let link = session.grant.sources[0].url
    let hls = null

    if (window.Hls && Hls.isSupported()) {
      hls = new Hls()
      hls.loadSource(link)
      hls.attachMedia(video)
    } else {
      video.src = link // Safari and iOS play HLS natively
    }
    video.play().catch(() => {})
    status.textContent = 'Playing'

    // Every heartbeat returns a fresh link. Keep the newest one.
    setInterval(async () => {
      try {
        const next = await call(`/v1/access/sessions/${session.id}/heartbeat`)
        link = next.grant.sources[0].url
      } catch (error) {
        status.textContent = 'Stopped: ' + error.message
        hls?.destroy()
      }
    }, session.heartbeat_interval * 1000)

    // The link in use expires after about 11 minutes. Swap to the newest one every 5 minutes.
    setInterval(() => {
      if (hls) hls.loadSource(link)
      else video.src = link
    }, 5 * 60 * 1000)
  }

  start().catch((error) => (status.textContent = error.message))
</script>

You should see the test pattern within a few seconds. Close the tab and the session lapses within 90 seconds. Open the page on a second device and, with replace_oldest, the first one stops at its next heartbeat.

If you get an error, read the code in the message. The usual ones are access_denied (no active entitlement for that customer), invalid_customer_token (see the reason in the details) and stream_disabled. All codes are on the error codes page.

What you built ​

You created a stream, granted access after a payment of your own, signed a customer token on your server and played a protected link in the browser. The same calls run a real night. Next:

  • Going live covers encoder settings and the checklist before showtime.
  • Paid access covers device limits, revoking and replays.

Vabu OS documentation