Guide

Streaming software integration

If you are building OBS, Meld Studio, Streamlabs or your own broadcaster, this is the whole surface: five endpoints that hand you a streamer's key and ingest URLs, read and write what their stream says it is, and rotate the key when it leaks. Everything acts as the person who authorised you.

Two things do the most damage if you get them wrong, so they lead: which host you dial, and which protocol you send. The endpoint reference is below them.

Ingest protocols

Velora accepts two ingest protocols. Both use the same stream key.

WHIP (WebRTC-HTTP Ingestion Protocol)

Recommended

Sub-second latency, and the only path that carries multiple quality levels. OBS 30+ and any WHIP-capable encoder.

WHIP settings
URL (the key is inside it)
https://publish.velora.tv/live/{streamKey}?direction=whip
Video / Audio
H.264 / Opus

Paste as Server and leave Bearer Tokenempty — the key is already in the URL, and filling that field is the most common mistake.

RTMP (Real-Time Messaging Protocol)

Works with every encoder. Higher latency than WHIP and a single quality level — no simulcast.

RTMP settings
Server
rtmp://rtmp.velora.tv/live
Stream Key
{streamKey}
Video / Audio
H.264 / AAC

Single video track only — we don't accept multi-track video over RTMP.

What the API gives you

The Velora Streaming API allows your application to:

  • Retrieve the user's stream key and ingest server URLs
  • Get and update stream title, category, and tags
  • Check live status and viewer count
  • Regenerate stream keys programmatically
  • List available categories for stream configuration

Base URL

All streaming API requests use the following base URL. Every path below is relative to it.

Base URL
https://api.velora.tv/api/integrations/oauth

Required scopes

Two of these are granted automatically. The two that touch the key or write to the channel need approval before your app can request them in production.

ScopePurposeApproval
stream:keyAccess stream key and ingest URLsRequired
stream:readRead stream info, status, categoriesAuto
stream:writeUpdate title, category, tagsRequired
user:readGet user profile informationAuto

Endpoints

In the order a broadcaster integration calls them: fetch the key, read what the stream says it is, change it, look up a category slug, and — rarely — rotate the key.

GET/stream/setupscope stream:key

Retrieve the user's stream key and all available ingest server URLs. This endpoint requires the stream:key scope.

Request — curl
curl https://api.velora.tv/api/integrations/oauth/stream/setup \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response 200
{
  "streamKey": "live_abc123xyz789",
  "whipUrl": "https://publish.velora.tv/live/live_abc123xyz789?direction=whip",
  "rtmpUrl": "rtmp://rtmp.velora.tv/live",
  "ingestServer": "publish.velora.tv",
  "preferredProtocol": "whip"
}

Note: The WHIP URL includes the stream key for WebRTC-based streaming.

GET/stream/infoscope stream:read

Get the current stream information including title, category, and live status. Requires stream:read scope.

Request — curl
curl https://api.velora.tv/api/integrations/oauth/stream/info \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response 200
{
  "title": "Building an awesome project!",
  "categoryName": "Software & Game Development",
  "categorySlug": "software-and-game-development",
  "isLive": true,
  "viewerCount": 142,
  "startedAt": "2026-01-19T02:30:00.000Z",
  "tags": ["coding", "chill", "flashing-lights"],
  "channelTags": ["vtuber", "lgbtqia"],
  "contentWarnings": ["flashing-lights"]
}
PUT/stream/infoscope stream:write

Update the stream title and/or category. Requires stream:write scope. All fields are optional.

Request body
FieldTypeDescription
titlestringStream title (max 140 characters)
categorySlugstringCategory slug (see /stream/categories)
tagsstring[]This stream's tags. Up to 7, plus any number of content warning tags, which never count toward the limit. Omit to leave them unchanged; send [] to clear. Stays set into the next stream until changed.
channelTagsstring[]The channel's identity tags (who the channel is, shown on the profile). Up to 5, plus any number of content warning tags. Omit to leave unchanged; send [] to clear.

Fields you leave out are left as they are. Tags are stored as slugs: sending "Speed Run" saves speed-run, exactly as if the streamer typed it in the dashboard. A list over the limit is refused with a 400 rather than trimmed, so your app always knows what was saved. The response shows the stored result.

Request — curl
curl -X PUT https://api.velora.tv/api/integrations/oauth/stream/info \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Building a chat bot with Node.js",
    "categorySlug": "software-and-game-development",
    "tags": ["coding", "chill", "flashing-lights"]
  }'
Response 200
{
  "success": true,
  "message": "Stream info updated",
  "title": "Building a chat bot with Node.js",
  "categorySlug": "software-and-game-development",
  "tags": ["coding", "chill", "flashing-lights"],
  "channelTags": ["vtuber", "lgbtqia"],
  "contentWarnings": ["flashing-lights"]
}
GET/stream/tagsscope stream:read

The curated tag vocabulary and the rules for setting tags, for building a tag picker. Each tag says which tier it fits (session for this stream, identity for the channel, or both) and whether it is a content warning. Custom tags are allowed too; this list is the common ground, not a limit.

Request — curl
curl https://api.velora.tv/api/integrations/oauth/stream/tags \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response 200
{
  "rules": {
    "streamTagLimit": 7,
    "channelTagLimit": 5,
    "contentWarningsCountTowardLimit": false,
    "customTagsAllowed": true,
    "slugFormat": "lowercase a-z, 0-9 and single hyphens, up to 40 characters; other input is converted"
  },
  "tags": [
    {
      "slug": "flashing-lights",
      "label": "Flashing Lights",
      "tier": "both",
      "category": "warning",
      "isContentWarning": true
    },
    {
      "slug": "speedrun",
      "label": "Speedrun",
      "tier": "session",
      "category": "content",
      "isContentWarning": false
    }
  ]
}
GET/stream/categoriesscope stream:read

Get a list of available stream categories. Use the slug value when updating stream info.

Request — curl
curl https://api.velora.tv/api/integrations/oauth/stream/categories \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response 200
{
  "categories": [
    {
      "slug": "just-chatting",
      "name": "Just Chatting",
      "imageUrl": "https://assets.velora.tv/categories/just-chatting.jpg"
    },
    {
      "slug": "software-and-game-development",
      "name": "Software & Game Development",
      "imageUrl": "https://assets.velora.tv/categories/dev.jpg"
    },
    {
      "slug": "music",
      "name": "Music",
      "imageUrl": null
    }
  ]
}
POST/stream/key/regeneratescope stream:key

Generate a new stream key for the user. The old key will immediately stop working. Requires stream:key scope.

Warning: This action is irreversible. The previous stream key will be invalidated immediately.

Request — curl
curl -X POST https://api.velora.tv/api/integrations/oauth/stream/key/regenerate \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response 200
{
  "streamKey": "live_newkey456def"
}

Complete example

Here's a complete example in JavaScript that fetches stream setup and updates stream info:

JavaScript — client
// Velora Streaming API Client Example

const VELORA_API = 'https://api.velora.tv/api/integrations/oauth';

class VeloraStreamClient {
  constructor(accessToken) {
    this.accessToken = accessToken;
  }

  async request(endpoint, options = {}) {
    const response = await fetch(`${VELORA_API}${endpoint}`, {
      ...options,
      headers: {
        'Authorization': `Bearer ${this.accessToken}`,
        'Content-Type': 'application/json',
        ...options.headers,
      },
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message || 'API request failed');
    }

    return response.json();
  }

  // Get stream key and ingest URLs
  async getStreamSetup() {
    return this.request('/stream/setup');
  }

  // Get current stream info
  async getStreamInfo() {
    return this.request('/stream/info');
  }

  // Update stream title and category
  async updateStreamInfo({ title, categorySlug, tags }) {
    return this.request('/stream/info', {
      method: 'PUT',
      body: JSON.stringify({ title, categorySlug, tags }),
    });
  }

  // Get available categories
  async getCategories() {
    return this.request('/stream/categories');
  }

  // Regenerate stream key
  async regenerateStreamKey() {
    return this.request('/stream/key/regenerate', { method: 'POST' });
  }
}

// Usage
const client = new VeloraStreamClient('your_access_token');

// Configure OBS with Velora credentials
async function configureOBS() {
  const setup = await client.getStreamSetup();

  console.log('Configure OBS with these settings:');
  console.log('Server:', setup.rtmpUrl);
  console.log('Stream Key:', setup.streamKey);

  return setup;
}

// Update stream before going live
async function prepareStream(title, category) {
  await client.updateStreamInfo({
    title,
    categorySlug: category,
  });

  console.log('Stream info updated!');
}

// Example: Set up a coding stream
configureOBS().then(setup => {
  prepareStream(
    'Building a Velora integration!',
    'software-and-game-development'
  );
});

Error handling

StatusMeaning
401Invalid or expired access token
403User doesn't have streaming enabled or missing required scope
400Too many tags (over 7 stream tags or 5 channel tags, not counting content warnings). Nothing is saved.
404Category not found (when updating stream info)
429Rate limit exceeded

Next steps