Skip to main content

How to write your own video content feed

Host your own JSON feed and Tappla builds your Apple TV, Roku and Fire TV apps from it. What we read, what breaks a feed, and how to check yours before it goes anywhere near a television.

Written by Robert Blessing

Use this guide if you host your videos on your own server or CDN and want to hand Tappla a feed you control. Your system decides what is in the app, how the rows are ordered and what is featured — none of that has to live in our dashboard.

The format is Roku's Direct Publisher JSON schema. If you already produce one of those, you are most of the way there. This page states exactly which parts of it we read.

The same feed drives Apple TV, Roku and Fire TV, so you maintain one catalog rather than three.

Check your feed before you rely on it

In your dashboard under Integrations there is a Check this feed button next to your feed URL, and pasted feeds are checked when you save them. It applies the same rules the import does, so a pass there means the import passes.

It is worth using. "Valid JSON" and "will produce a working app" are different questions, and a feed can be perfectly formed and still show nothing on a television.

Where we fetch it from

These are enforced. A feed that breaks any of them is refused, and your previous catalog keeps serving.

  • HTTPS only. Plain HTTP is refused.

  • A publicly reachable address. Internal or private hosts are blocked.

  • No redirects. The URL you enter must serve the feed itself.

  • Content-Type must contain application/json. A feed served as text/plain is refused even when the JSON is fine — this catches a lot of people hosting from a static file server.

We re-read your feed roughly every ten minutes, and immediately when you press Re-sync now. Keep the URL stable.

Sample feed

{
  "providerName": "ACME Corp",
  "lastUpdated": "2026-08-17T08:23:03Z",
  "playlists": [
    { "name": "latest-videos", "itemIds": ["jin4sce", "ubm6wzj"] }
  ],
  "categories": [
    { "playlistName": "latest-videos", "name": "My latest videos" }
  ],
  "shortFormVideos": [
    {
      "id": "jin4sce",
      "title": "Video title 1",
      "shortDescription": "Description of video 1",
      "thumbnail": "https://your-cdn.com/video1_thumbnail.jpg",
      "content": {
        "dateAdded": "2026-08-01T17:00:00Z",
        "duration": 339,
        "videos": [
          { "url": "https://your-cdn.com/video1.m3u8", "quality": "HD", "videoType": "HLS" }
        ]
      }
    }
  ]
}

What every video needs

  • id — unique and stable across the whole feed. This is what playlists point at.

  • title

  • content.videos — at least one entry. An item with none is skipped, and a row where every item is skipped disappears from the app. This is the single most common reason a feed imports and the app still looks empty.

  • content.dateAdded — recommended. Without it, sorting by date has nothing to sort on.

  • thumbnail — an absolute HTTPS URL, 16:9, JPEG. 1280x720 or 1920x1080 works well.

Ship HLS. Apple TV and Roku both need it for anything longer than a short clip. One adaptive master.m3u8 per video is the right shape — there is no benefit to listing renditions separately, the manifest already carries them.

Two rules that catch people out

A playlist name is an identifier, not a label. When you reorder or hide rows in the Tappla dashboard, we remember your choice against the playlist's name. Rename it in your feed and that row loses its saved position and visibility. If you want the visible label to change, keep name fixed and put the label in categories — that is exactly what the indirection is for.

Do not name a playlist "Live". The apps treat "live" as a keyword to pick the live presenter and playback path, and Tappla injects its own live row when you have a live stream configured. A playlist of your own with that name is dropped.

Two playlists sharing a name is fine — the second is shown as "Name (2)". Nothing is discarded.

Videos, movies, specials, live

shortFormVideos, movies, tvSpecials and liveFeeds are merged into one pool, and it makes no difference which you use. What matters is whether a playlist points at an item:

  • Referenced by a playlist, and it appears in that row.

  • Referenced by none, and it lands in an automatic row named after its array ("Movies", "Short Form Videos", and so on).

If you want control over your rows, reference every item from a playlist. The automatic rows exist so nothing silently disappears, not as a layout to rely on.

Members-only content

You can mark rows as members only directly in the dashboard, under Playlists & Order — no feed changes needed.

If you would rather drive it from your own system, add a tappla object to an item:

"tappla": {
  "access": "locked",
  "entitlementTags": ["premium"]
}

Namespaced under one key on purpose, so your feed stays a valid Direct Publisher document and you do not have to fork it. Everything in that object is optional, and a feed without it behaves exactly as it does today.

What we ignore

These are part of the Direct Publisher schema and cause no harm, but we do not currently use them: longDescription, genres, tags, rating, credits, releaseDate, content.captions, content.language, validity periods, ad breaks, and series/season/episode structures.

There is also no DRM: we play clear HLS.

Before you point us at a URL

  • HTTPS, public host, no redirect, Content-Type application/json

  • Every item has a unique stable id and at least one entry in content.videos

  • Streams are HLS (master.m3u8), typed "videoType": "HLS"

  • content.dateAdded present if you care about date ordering

  • Playlist names chosen as permanent identifiers, labels via categories

  • No playlist named "Live"

  • Thumbnails absolute HTTPS, 16:9, JPEG

  • Run Check this feed and read what it says

Stuck on something this page does not answer? Write to [email protected] with your feed URL and we will look at it with you.

Did this answer your question?