These page contains the Documentation of a Majority Touro.
The logged connection requests can be found here.
The radio queries a NTP server at time.wifiradiofrontier.com.
Tampering DNS to use a different server works.
Radio seems to query a server at update.wifiradiofrontier.com, see also https://github.com/cweiske/frontier-silicon-firmwares.
The host name where the radio expect the JSON-based API is airable.wifiradiofrontier.com. The A record points to 49.13.34.80 (May 2026) and this IP is also serves the older XML-API.
Forging DNS requests is used to redirect .wifiradiofrontier.com to alternative API implementations like Radio-API.
The Majority radio supports HTTPS and requires the API to be served via HTTPS. However, the radio does not validate any SSL certificates, so self-signed certificates work. Beside not verifying certificate chains, also common names are not verified. As HTTPS is supported, the radio can stream from HTTPS urls, but will also stream from HTTP.
I do not know if the radio supports IPv6, as the DNS records are all IPv4 and my DNS tampering uses only IPv4. As it is not possible to set IPv6 DNS servers in the radio's settings, I assume no IPv6 support.
The radio supports MP3 and other well known audio formats streams over HTTP. The radio does not support audio streams via HLS/M3U. The latter in contrast to older XML-based Radios.
- Host (main)
- https://airable.wifiradiofrontier.com
- 49.13.34.80 (10. May 2026)
- Normal HTTP 1.0 requests via SSL/ TLS on port 443
- SSL certificate is not checked/ verified
HTTP headers used by the radio:
Authorization: base64(<Radio-ID>:<??>)- The authentication mechanism of the radio, slightly inspired by HTTP basic auth.
- There is a Radio-ID, also visible in the radio's web interface, that provides the username. After a
:(for separation) some kind of token (32 chars, hex range) is provided as password. - I the assume the token being an md5 hash, it seems to be time-based, i.e., it changes over time for requests to the same URL.
Accept-Language: de-DE- The language of the server should respond in, e.g.,
de-DE,en-US.
- The language of the server should respond in, e.g.,
More headers sent by the radio:
Accept: audio/aac;mp3;dash: Even if the radio expects JSON- The
dashmay tell us that the radio supports DASH instead of HLS/M3U?
- The
User-Agent: ir-cui-...: Radio's firmware version
https://airable.wifiradiofrontier.com/
This is the first request by the radio to the server. It seems to be some kind of server compatibility check and the response needs to be quite exact.
So make sure to include airable and index in id. Also, the radio expect two urls/ content entries:
- The feeds (podcasts) site, a Directory Listing (below)
- The radios (stations) site, a Directory Listing (see below)
The response below is from Radio-APIs implementation, which points to two times to the same content entry (directory). In doing so, we can afterwards have subdirectories for podcasts, radio stations and everything else.
The radio will follow the url in the url field of each entry.
{
"id": [
"airable",
"directory",
"index"
],
"title": "Index",
"url": "https://radio.example.com",
"content": {
"entries": [
{
"id": [
"frontiersmart",
"service",
"feed"
],
"title": "Podcast",
"url": "https://radio.example.com/index"
},
{
"id": [
"frontiersmart",
"service",
"radio"
],
"title": "Radio",
"url": "https://radio.example.com/index"
}
]
}
}Shows the content of a directory. A directory can contain further directories or elements to play/ listen to.
- The second value of
idtells the radio that this is adirectory. - The radio will follow the
urlsin the content entries, if they are of typedirectory(again second value inid).
Example from Radio-API index, e.g., https://radio.example.com/index
{
"id": [
"frontiersmart",
"directory",
"list"
],
"title": "Liste", // name/ title shown in radio
"url": "https://radio.example.com/index?go=initial", // url to this page/ site
"content": {
"entries": [
{
"id": [
"frontiersmart",
"directory",
"podcast"
],
"title": "Podcast", // name to click on
"url": "https://radio.example.com/list?tid=3" // url will be opened by radio
// (as 'directory' because 'directory' in id above)
},
{
"id": [
"frontiersmart",
"directory",
"radio"
],
"title": "Radiosender",
"url": "https://radio.example.com/list?tid=1"
},
{
"id": [
"frontiersmart",
"directory",
"radiobrowser"
],
"title": "Radio-Browser",
"url": "https://radio.example.com/radio-browser?by=none&term=none"
},
{
"id": [
"frontiersmart",
"directory",
"guicodexxxx"
],
"title": "GUI-Code: Zxxx",
"url": "https://radio.example.com/?go=initial"
}
]
}
}So one item representing a radio station in the content.entries[] array.
- Second item in
idis radio, this defines the type! - Radio will not follow the url in the
urlfield!- It will concatenate the values in
idwith/as separator - So to open My Station the request will be sent to
https://radio.example.com/frontiersmart/radio/?sSearchtype=3&Search=1000and expects to get a Radio Station Infos (below).
- It will concatenate the values in
Example from Radio-API, we use http get values to be better compatible with the older XML-API.
{
"id": [
"frontiersmart",
"radio",
"?sSearchtype=3&Search=1000"
],
"title": "My Station",
"url": "https://radio.example.com/?sSearchtype=3&Search=1000",
"images": [
{
"url": "https://radio.example.com/media/default.png",
"size": [
150,
150
],
"type": "cover"
}
]
}There is a feed type, but for Radio-API a podcast is a directory of episodes, a this is easier to handle.
So we use directory when listing podcasts as entries, but the urls will respond with feed types, see Podcast Episodes (below).
The radio opened an url of the radio type, so second value of id is radio.
- The image will be shown by radio and Oktiv app.
- The
urlinstreamsmust not be a media stream url, instead there is one more level of JSON API (see Play Stream) below - Only the value for
urlinstreamseems to be relevant
{
"id": [
"frontiersmart",
"radio",
1000
],
"title": "Some station title",
"url": "https://radio.example.com/?sSearchtype=3&Search=1000",
"description": "some description",
"images": [
{
"url": "https://radio.example.com/media/default.png",
"size": [
150,
150
],
"type": "cover"
}
],
"streams": [
{
"url": "https://radio.example.com/?sSearchtype=3&Search=1000&play",
"codec": {
"name": "MP3",
"bitrate": 64
},
"reliability": 1
}
]
}This is a json response of type redirect (second value in id) and here we can link the actual media stream.
- The media stream is in
url - The
contentcontains the values of the, e.g., Radio Station Infos (above) of the stream
{
"id": [
"frontiersmart",
"redirect",
1000
],
"title": "Play Stream",
"url": "https://my-stream-server.example.com/some/radio/station.mp3",
"codec": {
"name": "MP3",
"bitrate": 64
},
"content": {
"title": "Some station title",
"url": "https://radio.example.com/?sSearchtype=3&Search=1000",
"description": "some description",
"images": [
{
"url": "https://radio.example.com/media/default.png",
"size": [
150,
150
],
"type": "cover"
}
],
"streams": [
{
"url": "https://radio.example.com/?sSearchtype=3&Search=1000&play",
"codec": {
"name": "MP3",
"bitrate": 64
},
"reliability": 1
}
]
}
}The radio opened an url of the decretory (as Radio-API does it) type, but second value of id is feed to inform radio about this being episodes of a podcast.
- Each entry is of type episode
- Episodes are similar to radio stations
- Again, there is an url for only fetching the episode details (here radio seems to follow the link)
- And a stream url, this stream url needs to be a json redirect like with stations, Play Stream (see above)
{
"id": [
"frontiersmart",
"feed",
3000
],
"title": "Podcast name",
"url": "https://radio.example.com/list?tid=3&id=3000",
"content": {
"entries": [
{
"id": [
"frontiersmart",
"episode",
"?sSearchtype=5&Search=3000X1"
],
"title": "Some Episode Name",
"url": "https://radio.example.com/?sSearchtype=5&Search=3000X1",
"description": "Some description",
"images": [
{
"url": "https://radio.example.com/image.php?hash=....",
"size": [
150,
150
],
"type": "cover"
}
],
"streams": [
{
"url": "https://radio.example.com/?sSearchtype=5&Search=3000X1&play",
"codec": {
"name": "MP3",
"bitrate": 64
},
"reliability": 1
}
]
},
{
"id": [
"frontiersmart",
"episode",
"?sSearchtype=5&Search=3000X2"
],
// next episode, ...
}
]
}
}{
"id": [
"frontiersmart",
"redirect",
"3000X200"
],
"title": "The title",
"url": "https://radio.example.com/?sSearchtype=5&Search=3000X200",
"description": "Some description",
"images": [
{
"url": "https://radio.example.com/image.php?hash=...",
"size": [
150,
150
],
"type": "cover"
}
],
"streams": [
{
"url": "https://radio.example.com/?sSearchtype=5&Search=3000X200&play",
"codec": {
"name": "MP3",
"bitrate": 64
},
"reliability": 1
}
]
}Like the Play Stream (see above)
{
"id": [
"frontiersmart",
"redirect",
"3000X200"
],
"title": "Episode abspielen",
"url": "https://my-stream.example.com/some-file-mp.mp3",
"codec": {
"name": "MP3",
"bitrate": 64
},
"content": {
"title": "The title",
"url": "https://radio.example.com/?sSearchtype=5&Search=3000X200",
"description": "Some description",
"images": [
{
"url": "https://radio.example.com/image.php?hash=...",
"size": [
150,
150
],
"type": "cover"
}
],
"streams": [
{
"url": "https://radio.example.com/?sSearchtype=5&Search=3000X200&play",
"codec": {
"name": "MP3",
"bitrate": 64
},
"reliability": 1
}
]
}
}
curl -k https://radio.example.com \
-H "Authorization: $(echo -n "AA00BB11CC22:$(echo -n "something"|md5)"|base64 )" \
-H "Accept-Language: de-DE" \
-H "Host: airable.wifiradiofrontier.com"