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
/mainfor about 30 minutes;/infoand/watchare 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.