← Modes

Creating a Custom Mode

A Custom Mode lets you plug your own content library into Combust Media. You run a small server that returns JSON, generate an encrypted mode code from this site, and share that code with users — they paste it into the app to unlock your content.

Combust Media never proxies your content: the app talks directly to your server using the URLs in your descriptor. You stay in full control of what you serve and to whom.


Overview

1. Build a server that answers the mode contract (JSON routes)
2. Write a mode descriptor (title, URLs, bearer token, …)
3. Generate an encrypted code here  →  Create a mode
4. Share the code — users add it in the app to unlock your mode

There are four media_type values:

media_type Player Flow Use for
vod_video Video Main → Info → Watch Movies, series
vod_audio Audio Main → Info → Watch Audiobooks, podcasts
live_tv Video Main → Watch Live TV channels
live_radio Audio Main → Watch Live radio stations

Mobile currently ships vod_video. The others are part of the contract and are rolling out.


Step 1 — Build your server

Your server exposes a few JSON endpoints. Every route must require your bearer token (sent as Authorization: Bearer <token> on each request).

Routes for vod_video / vod_audio

Route Required Returns
GET /main Yes Home-screen sections + items
GET /info Yes Detail for one item
GET /watch Yes Playback URL + subtitles
GET /search/[query] No Paginated search results

GET /main

{
  "sections": [
    {
      "title": "Trending Movies",
      "media_items": [
        {
          "id": "movie1",
          "title": "John Wick: Chapter 4",
          "image": "https://img.example.com/3.jpg",
          "type": "movie",
          "release_date": "2023",
          "duration": "120m",
          "info_url": "https://yourserver.com/info?id=movie1"
        }
      ]
    }
  ]
}

Each item's type is movie or series. info_url is called next. Important: every URL you embed (info_url, watch_url) must be reachable from the user's device — use your public host, not localhost.

GET /info

The app calls the item's info_url. episodes is always an array — a single entry for a movie, one per episode for a series.

{
  "id": "tv1",
  "title": "Platonic",
  "cover": "https://img.example.com/cover_1.jpg",
  "image": "https://img.example.com/1.jpg",
  "description": "A long description…",
  "type": "series",
  "release_date": "2023-05-23",
  "duration": "45 min",
  "rating": "9.0",
  "episodes": [
    { "title": "Eps 1: Pilot", "episode": "1", "season": "1", "watch_url": "https://yourserver.com/watch?id=tv1_1" }
  ]
}

For a movie, episodes is [{ "watch_url": "https://yourserver.com/watch?id=movie1" }].

GET /watch

The app calls the watch_url. Return the direct media URL (e.g. an HLS .m3u8) and any subtitles (empty array if none).

{
  "url": "https://yourserver.com/media/file.m3u8",
  "subtitles": [
    { "url": "https://yourserver.com/subs/eng.srt", "lang": "English" },
    { "url": "https://yourserver.com/subs/eng-sdh.vtt", "lang": "English (SDH)" }
  ],
  "headers": { "Referer": "https://yourserver.com/" }
}

Subtitles are sidecar files in SubRip (.srt) or WebVTT (.vtt); the app fetches and renders them itself, stripping any HTML or SSA styling. They start off — the viewer picks one from the player's subtitle menu, in the order you return them. lang is the label shown in that menu, exactly as written, so make it tell entries apart: "English" and "English (SDH)", not "en" twice.

headers is optional. Some stream hosts only serve media to requests carrying a particular Referer or User-Agent; whatever you put here is sent with the media URL and its HLS segments. It is not sent when fetching subtitles — those URLs must be reachable with a plain request.

GET /search/[query]

Only if you set a search URL. The app appends the URL-encoded query and an optional ?page.

{
  "next_page_url": "https://yourserver.com/search/rick?page=2",
  "prev_page_url": null,
  "media_items": [ /* same shape as /main items */ ]
}

Live routes (live_tv / live_radio)

Live content skips the info step — tapping a channel goes straight to playback. /main returns channels (with logo, watch_url, optional info_url for EPG) instead of media_items, and /watch returns just url.


Step 2 — Write your descriptor

Your descriptor is the set of fields below. You don't write JSON by hand — the mode builder form collects them.

Field Required Notes
title Yes Display name
description Yes What the mode provides
media_type Yes One of the four types
logo_url Yes Square image, ≥200×200
bearer_token Yes Token your server checks on every request
main_screen_url Yes Your /main URL
search_screen_url No Your /search URL

Modes have no developer-settable expiry. Every mode ships locked — access is granted per person with a renewal code (see below).


Step 3 — Generate your code

Open the mode builder, fill in the fields, and click Generate code. You'll get a string that starts with v1:. The descriptor is encrypted server-side with a key that never leaves Combust Media — the raw fields (including your bearer token) are never exposed in the code.


Step 4 — Share it

Give the v1:… code to your users however you like (email, a web page, a private group). In the app they open Custom → Add, paste the code, and your mode appears — but it stays locked until they redeem a renewal code, so pair it with one per user (see below). You can also add codes yourself on the Custom modes page.


Access & renewal codes

A mode code grants no access on its own — every mode expires the moment it's created, so it lands in a user's app locked. Access is granted per person with a renewal code:

  • You mint renewal codes on the renewal codes page. Each carries a grants access until date and is single-use (self-expires if unredeemed).
  • The user redeems a code (XXXX-XXXX) in the app, which sets their expiry to that date.
  • Redeeming again with a later date extends them; mint one code per person.

So distribute the v1:… mode code together with a renewal code for each user. This runs subscription-style, per-person access — different people, different expiry dates, one mode — with no payments handled inside the app.


Good to know

  • Caching: the app caches /main for about 30 minutes; /info and /watch are always fetched fresh, so playback URLs can be short-lived or signed.
  • Security: your bearer token authorizes every request — rotate it (with a new code) if it leaks. The encrypted code is safe to distribute; the underlying token is not embedded in plaintext.
  • Reachability: test that every URL your server returns loads from a phone on mobile data, not just your local network.