For bridge authors

Custom catalog API

Host a JSON endpoint, add the URL under Sources → JSON, and your library shows up in JAVP as posters, shelves, series and episodes.

JAVP scrapes nothing. Your bridge maps your own library — Plex, Jellyfin, a folder of files, magnets you have the rights to — into the schema below.

Modes

ModeWhen to useHow JAVP uses it
v1 — bulk dump Small and medium libraries (rough guide: under ~5–10k titles) One GET, then all items are imported into the on-device catalog
v2 — query API Large libraries Search, browse and page remotely; only what the user opens gets cached

Both are implemented. Prefer v2 when a full dump would be huge. In-app search filters locally synced items and, for v2 sources, also calls /search.

v1 — Bulk catalog

Request

GET {catalogUrl}
Accept: application/json

Response — object form (preferred)

{
  "name": "My Home Library",
  "version": 1,
  "items": [
    {
      "id": "movie-42",
      "title": "Big Buck Bunny",
      "playUrl": "https://cdn.example.com/bbb.mp4",
      "kind": "vod",
      "thumbnailUrl": "https://cdn.example.com/bbb.jpg",
      "group": "Open Movies",
      "subtitle": "2008 · Blender Foundation",
      "durationMs": 596000,
      "audioLanguages": ["en"],
      "subtitleLanguages": ["en", "fr"],
      "subtitles": [
        {
          "url": "https://cdn.example.com/bbb.en.vtt",
          "language": "en",
          "label": "English",
          "default": true
        }
      ],
      "segments": [
        { "type": "intro", "startMs": 0, "endMs": 90000 }
      ]
    }
  ]
}

Response — bare array (also accepted)

[
  {
    "title": "Live News",
    "url": "https://cdn.example.com/news.m3u8",
    "kind": "live"
  }
]

Aliases for the items list: items, entries or media.

Item fields

FieldRequiredNotes
titleyesDisplay name
playUrlyes*Stream URL, local path or magnet:?…. *Not required for kind: "series" shells, or rows that only carry playVariants
url / streamUrl*Aliases for playUrl
idnoStable id — strongly recommended. Auto-generated when omitted, which breaks clean re-sync
kindnovod (default), live, series, network, local, catchup
thumbnailUrlnoPoster or logo. Aliases: poster, logo
groupnoCategory / shelf. Alias: category
subtitlenoSecondary display line such as year or genre — not a caption track
durationMsnoDuration in milliseconds
channelId / streamId / epgChannelIdnoIPTV-style ids
catchupDaysnoArchive window for live and catch-up
tmdbIdnoTMDB movie or TV id — enables skip-intro and enrichment
anilistIdnoAniList media id (integer) — anime bridges and enrichment
imdbIdnoIMDb id (tt…) for IntroDB / TheIntroDB
tvdbIdnoTheTVDB id
torrentFile / fileHintnoPreferred file name, or a substring of it, inside a multi-file magnet
poster / posterUrlnoPortrait poster — preferred for VOD shelves
backdrop / backdropUrlnoBackdrop / hero art
plot / descriptionnoSynopsis for the detail screen
genresnoArray of strings, or a comma-separated string
ratingnoNumeric rating, e.g. 7.8
yearnoRelease year
releaseDatenoISO date string when known
trailerUrlnoDirect trailer URL, or a YouTube watch URL
trailerKey / youtubeTrailernoYouTube video id
castno["Name"], or [{ "name", "character", "profileUrl", "order" }]
season / seasonNumbernoEpisode season
episode / episodeNumbernoEpisode number
seriesId / parentIdnoLinks an episode row to a series shell id
seasonsnoNested season/episode tree on a series shell — see below
audioLanguagesno["ja","en"] or "ja,en". Aliases: audio, audioLangs
subtitleLanguagesnoKnown subtitle languages. Aliases: subLanguages, subtitleLangs, subs
subtitlesnoExternal subtitle files. Alias: externalSubtitles
audioTracksnoExternal audio files. Aliases: externalAudio, audioFiles
httpHeaders / headersnoMap of headers used when opening the stream
segmentsnoSkip windows: intro, credits and so on
playVariants / variantsnoQuality or edition ladder, expanded into sibling rows
contentRating / certificationnoe.g. PG-13, TV-MA
studio / networknoStudio or network label
originalTitlenoOriginal-language title
tagsnoFreeform tags, array or CSV
resolutionnoe.g. 1080p, 4K
videoCodec / audioCodecnoe.g. hevc, aac
hdrnoe.g. HDR10, DV
updatedAtnoISO timestamp for the row

Rows are skipped when they have no title, or no playable URL while not being a series shell or a variant parent.

Series

The episode picker reads only one of these two things:

  1. Nested seasons[].episodes[] on the series shell — preferred, or
  2. Flat catalog rows carrying seriesId plus seasonNumber / episodeNumber
RuleDetail
Shell playUrlOptional. Never treated as "the only playable thing"
Empty seasonsEmpty episode UI, even when the shell has a magnet or playVariants
Episode playUrlOptional on stubs; fill it later via /items/{episodeId}, or include it on /items/{id}
Shell playVariantsShow-level editions only — not the episode list

Option A — nested seasons on a shell

Preferred for bring-your-own and anime bridges.

{
  "id": "anilist-154587",
  "title": "Frieren: Beyond Journey's End",
  "kind": "series",
  "anilistId": 154587,
  "posterUrl": "https://cdn.example.com/show.jpg",
  "plot": "…",
  "seasons": [
    {
      "seasonNumber": 1,
      "name": "Season 1",
      "episodes": [
        {
          "id": "anilist-154587-s1e1",
          "episodeNumber": 1,
          "title": "The Journey's End",
          "playUrl": "magnet:?xt=urn:btih:…"
        }
      ]
    }
  ]
}

Option B — flat episode rows

[
  { "id": "show-1", "title": "Example Show", "kind": "series", "posterUrl": "…" },
  {
    "id": "show-1-s1e1",
    "title": "Pilot",
    "playUrl": "https://cdn.example.com/s1e1.mp4",
    "seriesId": "show-1",
    "seasonNumber": 1,
    "episodeNumber": 1
  }
]

Batch and multi-file magnets

Setting the same batch magnet as playUrl on every episode is valid. The torrent engine picks a file by, in order:

  1. torrentFile / fileHint on the episode, else
  2. Matching episodeNumber — and seasonNumber when present — against file names such as S01E02, - 02 ( or E02, else
  3. The largest streamable file, as a legacy fallback

Play variants

"playVariants": [
  { "id": "hd", "label": "1080p", "playUrl": "https://…/1080.mp4", "resolution": "1080p" },
  { "id": "uhd", "label": "4K", "playUrl": "https://…/2160.mp4", "resolution": "4K", "hdr": "HDR10" }
]
WhereBehaviour
VOD / movie rowExpanded into sibling rows, shown as Versions chips on the detail screen
Series shellShow-level extras only; ignored by the episode picker
Nested or flat episode rowPer-episode quality ladder, expanded the same way as VOD

External subtitles

"subtitles": [
  {
    "url": "https://cdn.example.com/en.vtt",
    "language": "en",
    "label": "English",
    "default": true,
    "format": "vtt"
  },
  {
    "url": "https://cdn.example.com/en.forced.srt",
    "language": "en",
    "forced": true
  },
  {
    "url": "https://cdn.example.com/en.sdh.vtt",
    "language": "en",
    "sdh": true
  }
]
FieldRequiredNotes
urlyesAliases: uri, src
languagenoAlias: lang
labelnoAliases: title, name
defaultnoAuto-selected on play. Alias: isDefault
forcednoAlias: isForced
hearingImpaired / sdh / ccnoSDH / captions flag
formatnosrt, vtt, ass

External audio

"audioTracks": [
  { "url": "https://cdn.example.com/ja.mka", "language": "ja", "label": "Japanese", "default": true },
  { "url": "https://cdn.example.com/en.mka", "language": "en", "label": "English" }
]

HTTP headers

"httpHeaders": {
  "User-Agent": "JAVP",
  "Referer": "https://cdn.example.com/"
}

Passed to the player when it opens playUrl.

Skip segments

"segments": [
  { "type": "intro", "startMs": 90000, "endMs": 150000 },
  { "type": "credits", "startMs": 5400000 }
]

Types: intro / opening, recap, credits / outro, preview. start and end in seconds are also accepted when the *Ms form is omitted.

Playback URLs

  • HTTP(S) progressive, HLS or DASH — played directly
  • magnet:?… — resolved by the bring-your-own torrent engine into a localhost HTTP stream
  • File paths — treated as local media where applicable

v2 — Query API

Point Sources → JSON at the catalog root. JAVP detects version: 2, or a non-empty capabilities array, and stops expecting a full item dump.

GET /catalog — describe the catalog

Your source URL can be this document, or any URL that returns:

{
  "name": "Huge Library",
  "version": 2,
  "capabilities": ["search", "browse", "groups"],
  "itemCount": 128400
}

If that same response also includes items, they are imported as a warm cache.

GET /search?q=bunny&page=1&limit=50
{
  "query": "bunny",
  "page": 1,
  "limit": 50,
  "total": 3,
  "items": [ { "id": "…", "title": "…", "playUrl": "…", "kind": "vod" } ]
}

Wired to the in-app Search screen for v2 sources.

GET /browse?group=&page=&limit=

Lists a shelf or category without a text query.

GET /items/{id}

Fetches one title, including cast, trailer and nested seasons when available. Series shells may return either:

  • Full seasons[].episodes[] with playUrls, or
  • Season or episode stubs without playUrl, for progressive detail

Optional: GET /items/{id}/episodes?season=

A lazy episode list, so you do not have to resolve every magnet on first open.

GET /items/anilist-154587/episodes?season=1
{
  "season": 1,
  "episodes": [
    {
      "id": "anilist-154587-s1e1",
      "episodeNumber": 1,
      "title": "The Journey's End",
      "playUrl": "magnet:?xt=urn:btih:…"
    }
  ]
}

A top-level seasons array or a bare episode array are also accepted. When episode stubs omit playUrl, JAVP calls GET /items/{episodeId} on play.

Optional: GET /groups

{
  "groups": [
    { "id": "movies", "name": "Movies", "count": 4200 },
    { "id": "kids", "name": "Kids", "count": 310 }
  ]
}

Client behaviour

  1. Source is added, JAVP detects version: 2 or capabilities
  2. No bulk items dump is required
  3. Search calls /search and caches the hits locally
  4. Continue-watching and detail screens use cached rows, plus /items/{id} when needed
  5. Progressive series detail — first paint comes from /items/{seriesId} and stubs are fine; playUrl and playVariants resolve on Play or Versions via /items/{episodeId}
  6. Simulcasts and new episodes — shell episode lists are soft-revalidated on open after roughly 30 minutes, and on pull-to-refresh. New episode ids merge in, and already-resolved magnets are kept when the bridge still returns stubs for those ids

URL joining. When the source URL ends in /catalog or a .json file, query paths are resolved from the parent directory — so https://host/nyaa/catalog becomes https://host/nyaa/search.

Tips for bridge authors

  1. Emit stable ids so a re-sync updates rows instead of duplicating them.
  2. Keep v1 payloads to a few MB where you can; past that, switch to v2.
  3. Use HTTPS. Cleartext HTTP still works on Android for IPTV-style setups, but it is discouraged.
  4. For series, emit nested seasons or flat seriesId rows. A shell magnet on its own will not populate the episode UI.
  5. Magnets in playUrl are for legal bring-your-own torrents only. Batch magnets shared across episodes are supported, with per-episode file selection.
  6. Put cast, trailer and seasons in the item — or in v2 /items/{id} — so the detail screen works without TMDB.
  7. Prefer anilistId for anime bridges; tags: ["mal:…"] stays optional.
  8. For large series, use /items/{id}/episodes?season=, or stub the episode ids and fill them in later.

A website can deep-link into JAVP so a visitor adds a catalog, an M3U playlist or an Xtream login in one tap. The app always shows a confirmation dialog first.

Prefer HTTPS App Links for public “Add to JAVP” buttons. With the app installed, Android opens JAVP directly. Without it, the browser loads /add — install from the download page, then open the same link again. Nothing is added automatically after install (a Play Store listing URL cannot do that either without extra deferred-link plumbing).

Keep javp://add?… for QR codes, TV pairing paste, and non-HTTPS contexts.

Custom JSON catalog

https://javp.app/add?type=custom&url=https%3A%2F%2Fexample.com%2Fcatalog.json&name=My%20Library
javp://add?type=custom&url=https%3A%2F%2Fexample.com%2Fcatalog.json&name=My%20Library
QueryRequiredNotes
typeyescustom, or the aliases json / catalog
urlyesHTTP(S) catalog URL. catalog and playlist also accepted
namenoDisplay name in Sources

As an HTML link:

<a href="https://javp.app/add?type=custom&url=https%3A%2F%2Fexample.com%2Fcatalog.json&name=My%20Library">
  Add to JAVP
</a>

M3U playlist

https://javp.app/add?type=m3u&url=https%3A%2F%2Fexample.com%2Flist.m3u&name=My%20IPTV&epg=https%3A%2F%2Fexample.com%2Fepg.xml
javp://add?type=m3u&url=https%3A%2F%2Fexample.com%2Flist.m3u&name=My%20IPTV&epg=https%3A%2F%2Fexample.com%2Fepg.xml
QueryRequiredNotes
typeyesm3u, or the aliases m3u8 / playlist
urlyesPlaylist URL
namenoDisplay name
epgnoOptional EPG XML URL. epgUrl also accepted

Xtream Codes

https://javp.app/add?type=xtream&url=http%3A%2F%2Fexample.com%3A8080&username=user&password=pass&name=My%20IPTV&alt=http%3A%2F%2Falt.example.com
javp://add?type=xtream&url=http%3A%2F%2Fexample.com%3A8080&username=user&password=pass&name=My%20IPTV&alt=http%3A%2F%2Falt.example.com
QueryRequiredNotes
typeyesxtream, or the aliases xc / xtream-codes
urlyesServer DNS. server, host and dns also accepted
usernameyesAlso user / login
passwordyesAlso pass / pwd
namenoDisplay name
altnoOptional Samsung/LG DNS. alternate, altDns and dns2 also accepted

The confirmation dialog shows the server and username only — the password is never displayed. Prefer HTTPS where the portal supports it, since credentials in a URL can end up in browser history and share sheets.

If the same type and URL — or the same Xtream server and username — is already present, JAVP offers a re-sync instead of duplicating the source. Jellyfin, Emby and Plex are not supported through deep links.

Phone deep link vs Android TV QR

HTTPS and javp:// share the same query string:

SurfaceHow
Phone or tablet with JAVPTap https://javp.app/add?… (or javp:// / a QR), then confirm — the source syncs on that device
Android TVThe TV shows a LAN QR; on your phone browser, paste the same javp://add link or fill the form, and it posts to the TV

So you can publish one "Add to JAVP" HTTPS link for phones, and tell TV users to open Sources → Add with phone and paste the javp:// form of the same query. There is no second parameter set to maintain.

Where it shows up in the app

SurfaceBehaviour
Sources → JSONAdd a catalog URL and sync, whether it is a v1 dump or a v2 descriptor
https://javp.app/add?… / javp://add?…Confirm, then add and sync a custom, M3U or Xtream source
TV pairing formPaste javp://add, or fill the fields manually
Home and SearchLocal filtering, plus remote /search for v2
Title detailCast, trailer, audio and subtitle tracks, versions, technical tags
PlayerHTTP headers, external audio and subtitles, skip segments
Library → magnetA separate bring-your-own torrent entry, not a remote catalog

An XML flavour using the same field names may come later. For now JSON is the supported format.


Building a bridge and something here does not match what the app does? Email [email protected].