Roku ships an HTTP API on every device they sell. It has no authentication, no API key, no documentation page on a marketing site — but it powers every third-party Roku remote app on the App Store and Play Store. It's called ECP (External Control Protocol) and once you've seen it, you'll wonder why the rest of the smart-TV world isn't this simple.
I needed an in-browser remote for HiRemote's Hisense Roku TV landing page — the idea being a visitor who lands on the page can press buttons in the page itself without first installing the iOS app. Three quirks made it harder than the marketing-page version suggests; here's the actual implementation.
1. Discovery — SSDP is overrated, the user types the IP
Every Roku tutorial starts with "use SSDP multicast on 239.255.255.250:1900". This is true but useless from a browser: browsers can't send UDP. You can't run SSDP from JavaScript.
For a browser-based remote, the pragmatic solution is: ask the user for their TV's IP. On the iPhone app side we use Bonjour. On the web page side we just show a one-time input box, then localStorage it. Roku TVs always run ECP on port 8060, so once you have the IP, the base URL is fixed:
const baseUrl = `http://${tvIp}:8060`
The only gotcha here is that the user's browser is HTTPS but the TV is HTTP. Modern browsers block mixed-content requests, so you have to either accept this and let the request fail gracefully, or proxy through your own backend. We chose the first option — the input box explains the limitation and tells the user to also try the iOS app which doesn't have this restriction.
2. Buttons — every command is a POST to a path
The control surface is one of the cleanest API designs I've seen in a consumer device:
POST http://<tv-ip>:8060/keypress/<KeyName>
<KeyName> is one of about 30 documented strings: Home, Up, Down, Left, Right, Select, Back, Play, Pause, Rev, Fwd, VolumeUp, VolumeDown, VolumeMute, PowerOn, PowerOff, plus a few platform-specific ones for Roku TV (channel up/down, input switching).
No body, no headers, no auth. Just the POST:
async function sendKey(key: RokuKey) {
return fetch(`${baseUrl}/keypress/${key}`, {
method: "POST",
})
}
For typing into search boxes (Netflix login, YouTube search), there's /keypress/Lit_<urlEncodedChar> — one POST per character. Cleaner than building a virtual keyboard, ugly that it isn't batched, but it works.
3. Direct-app-launch — the surprisingly useful one
The endpoint nobody talks about:
POST http://<tv-ip>:8060/launch/<channel-id>
Channel IDs are stable Roku-assigned numbers. 12 is Netflix, 13 is Prime Video, 837 is YouTube, 291097 is Disney+. Posting to /launch/12 boots Netflix on the TV — no D-pad navigation needed.
This is the killer feature for a remote that lives on a phone or in a browser: you skip the entire "navigate the home screen" UX that makes physical Roku remotes annoying. One tap → on Netflix.
Full list of channel IDs is in the device's response to GET /query/apps (returns XML, so use DOMParser not JSON.parse).
4. Putting it together
type RokuKey =
| "Home" | "Back" | "Select"
| "Up" | "Down" | "Left" | "Right"
| "VolumeUp" | "VolumeDown" | "VolumeMute"
| "PowerOn" | "PowerOff"
| "Play" | "Pause" | "Rev" | "Fwd"
class RokuRemote {
constructor(private tvIp: string) {}
private base() { return `http://${this.tvIp}:8060` }
press(key: RokuKey) {
return fetch(`${this.base()}/keypress/${key}`, { method: "POST" })
}
type(text: string) {
return Promise.all(
[...text].map(ch =>
fetch(`${this.base()}/keypress/Lit_${encodeURIComponent(ch)}`, { method: "POST" })
)
)
}
launchApp(channelId: number) {
return fetch(`${this.base()}/launch/${channelId}`, { method: "POST" })
}
}
That's the whole remote. Render a D-pad with onClick={() => remote.press("Up")} and you have a working web-based Roku remote in 80 lines.
TL;DR
Roku's HTTP control protocol is plain POST /keypress/<KeyName>. No auth. 80 lines of TypeScript = working remote. Discovery is the only genuinely hard part for a browser context, and "ask the user for the IP" is the right answer there.
The end result is live at hiremote.app/hisense-roku-tv-remote — bring your own Roku-TV IP and try it without installing anything.
Top comments (0)