When a publisher recently asked if Zype offered an embeddable Electronic Program Guide (EPG) widget for websites, the short answer was: not yet. But that didn’t mean they were out of options. In fact, with just a bit of creativity—and the help of Zype’s Playout EPG APIs and a commercial LLM like ChatGPT—I was able to build exactly what they needed in under 30 minutes.
This wasn’t a team of frontend developers with weeks of runway. It was one person (me), a linear stream powered by Zype’s Playout service, and a simple goal: display a real-time program guide on an existing website, styled to match a specific brand, without a full product build.
The Challenge: A Custom Guide Without the Custom Work
Zype Playout already supports EPGs across connected TV and mobile apps through its Apps Creator platform (you can view reference docs here). But on the web, we find some publishers want tighter control: their own HTML experience, integrated directly into their site’s layout and SEO framework.
Rather than wait for a productized embeddable widget or hire out custom development for an ask like this, I experimented with a rapid prototyping approach using ChatGPT.
The Magic: Zype's Playout XMLTV EPG API Feed + ChatGPT
Every Zype Playout stream includes an XMLTV feed—an industry-standard format that lists program schedules, titles, descriptions, and more. It’s the same structured metadata used by many OTT platforms to display program guides.
With this feed in hand, I dropped it into ChatGPT and simply asked:
“Can you help me create an HTML-based EPG using this XMLTV file?”
The response: a resounding yes!
From Concept to Code in Minutes
To guide the output, I shared examples of what I had in mind—screenshots of popular grid-style guides, layout expectations, and specific formatting instructions. With each prompt, the generated code became more refined:- Titles pulled from <title>
- Descriptions from <desc>
- Time blocks aligned to the schedule
- Subtitles and metadata included where available
- CSS styles matching the desired look and feel
After just a few iterations, I had a working, styled HTML+JavaScript EPG snippet—ready to embed directly into a live site.
Here are some prompt examples I used during my conversation:
- Prompt: Please provide a simple HTML/JavaScript version, not a reactive native for this use case.
- Prompt: Can you replace references of the XMLTV automatically from this Zype XMLTV EPG API
https://api.zype.com/scheduler/v1/channels/{PLAYOUT_CHANNEL_ID}/published/rundown/xmltv.xml?api_key=ZYPE_API_KEY&mode=hybrid&hours=336 - Prompt: This should be one channel. Can you use the <title> in the <programme> block? The start and stop elements in <programme> block should dictate when a show starts and stops within the UX.
- Prompt: Can you style the guide so that it includes the following features?
- A dark theme with contrasting highlight colors for active and upcoming programs
- A structured grid layout that separates current, next, and later programming clearly
- A progress bar visualizing time elapsed and time remaining for the currently airing program
- Improved spacing and readable fonts to enhance UX and legibility across desktop and mobile
- Dynamically populate all data (program title, start/end times, descriptions) by parsing the XMLTV feed.
Fully Custom, Fully Yours
The final result wasn’t a generic iframe. It was a real, responsive program guide that could be styled, extended, and embedded anywhere. And because it relied solely on standard web tech and Zype’s APIs, there were no dependencies or lock-in—just clean code that worked with an existing publishing environment.
Quickly Iterate with Zype's Flexible, API-First Platform
For teams using Zype Playout to power linear streams, this example highlights what’s possible when an API-first platform meets modern no-code tools. With just Zype’s XMLTV EPG API feed and a commercial LLM like ChatGPT, teams can quickly spin up a custom, branded EPG—without waiting on roadmap items or investing in full-scale frontend development.
This kind of flexibility is especially valuable for smaller teams who need to move fast, maintain UX control, and avoid third-party dependencies. It’s a glimpse into the future of building: rapid, iterative, and powered by composable APIs and AI.
If you’re interested in learning more about how Zype’s APIs can help you create efficient video operations, request a meeting with our team and we’ll be happy to discuss your specific needs. If you’d like to experiment with implementing your own EPG on your website, here is the full code that ChatGPT provided for you to try.
Note: In order to make this code work, you need to provide a Zype API Key and Playout Channel ID!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Free Speech TV</title>
<style>
body { font-family: Arial, sans-serif; background-color: #000; color: #fff; }
.epg-container { width: 100%; overflow-x: auto; padding: 20px; display: flex; flex-direction: row; position: relative; }
.epg-header { display: flex; justify-content: center; align-items: center; padding: 10px 20px; background: #111; font-size: 18px; font-weight: bold; }
.epg-info { display: flex; justify-content: space-between; padding: 10px; background: #222; margin-bottom: 10px; align-items: center; }
.epg-section { flex: 1; padding: 10px; text-align: center; background: #333; margin: 5px; border-radius: 5px; }
.epg-section h2 { font-size: 16px; margin-bottom: 5px; }
.epg-row { display: flex; flex-direction: column; align-items: center; padding: 10px; margin-right: 10px; border-radius: 5px; background: #222; min-width: 250px; position: relative; }
.epg-program { font-weight: bold; margin-bottom: 5px; text-align: center; font-size: 18px; }
.epg-time { font-size: 14px; color: #bbb; margin-bottom: 5px; }
.epg-description { font-size: 12px; color: #ddd; text-align: center; margin-top: 5px; }
.progress-bar { width: 100%; height: 5px; background: #444; position: relative; margin-top: 5px; }
.progress-bar .progress { height: 100%; background: #9147ff; width: 0%; transition: width 1s linear; }
</style>
</head>
<body>
<div class="epg-header">Now: <span id="currentTime"></span></div>
<div class="epg-info">
<div class="epg-section">
<h2>Currently Playing</h2>
<div id="currentProgramTitle">Loading...</div>
<div id="currentProgramTime"></div>
<div class="progress-bar"><div class="progress" id="progressCurrent"></div></div>
</div>
<div class="epg-section">
<h2>Next</h2>
<div id="nextProgramTitle">Loading...</div>
<div id="nextProgramTime"></div>
</div>
<div class="epg-section">
<h2>Later</h2>
<div id="laterProgramTitle">Loading...</div>
<div id="laterProgramTime"></div>
</div>
</div>
<div class="epg-container" id="epgContainer"></div>
<script>
async function loadEPG() {
const response = await fetch('https://api.zype.com/scheduler/v1/channels/{PLAYOUT_CHANNEL_ID}/published/rundown/xmltv.xml?api_key={ZYPE_API_KEY}&mode=hybrid&hours=336');
const text = await response.text();
const parser = new DOMParser();
const xml = parser.parseFromString(text, "text/xml");
const epgContainer = document.getElementById("epgContainer");
const programs = xml.getElementsByTagName("programme");
const nowEpoch = new Date().getTime();
let currentProgram = null;
let nextProgram = null;
let laterProgram = null;
for (let i = 0; i < programs.length; i++) {
const program = programs[i];
const title = program.getElementsByTagName("title")[0].textContent;
const descriptionElement = program.getElementsByTagName("desc")[0];
const description = descriptionElement ? descriptionElement.textContent : "No description available";
const startEpoch = parseInt(program.getAttribute("start"));
const stopEpoch = parseInt(program.getAttribute("stop"));
const startTime = formatEpochTime(startEpoch);
const endTime = formatEpochTime(stopEpoch);
if (nowEpoch >= startEpoch && nowEpoch < stopEpoch) {
currentProgram = { title, startTime, endTime, startEpoch, stopEpoch };
} else if (startEpoch > nowEpoch && !nextProgram) {
nextProgram = { title, startTime, endTime };
} else if (startEpoch > nowEpoch && !laterProgram && nextProgram.title !== title) {
laterProgram = { title, startTime, endTime };
}
const epgRow = document.createElement("div");
epgRow.classList.add("epg-row");
epgRow.innerHTML = `
<div class="epg-program">${title}</div>
<div class="epg-time">${startTime} - ${endTime}</div>
<div class="epg-description">${description}</div>
`;
epgContainer.appendChild(epgRow);
}
if (currentProgram) {
document.getElementById("currentProgramTitle").textContent = currentProgram.title;
document.getElementById("currentProgramTime").textContent = `${currentProgram.startTime} - ${currentProgram.endTime}`;
updateProgressBar(currentProgram.startEpoch, currentProgram.stopEpoch);
}
if (nextProgram) {
document.getElementById("nextProgramTitle").textContent = nextProgram.title;
document.getElementById("nextProgramTime").textContent = `${nextProgram.startTime} - ${nextProgram.endTime}`;
}
if (laterProgram) {
document.getElementById("laterProgramTitle").textContent = laterProgram.title;
document.getElementById("laterProgramTime").textContent = `${laterProgram.startTime} - ${laterProgram.endTime}`;
}
}
function formatEpochTime(epoch) {
const date = new Date(epoch);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
function updateProgressBar(startEpoch, stopEpoch) {
const nowEpoch = new Date().getTime();
const progress = ((nowEpoch - startEpoch) / (stopEpoch - startEpoch)) * 100;
document.getElementById("progressCurrent").style.width = `${Math.min(100, Math.max(0, progress))}%`;
}
setInterval(updateProgressBar, 1000);
loadEPG();
</script>
</body>
</html>
{% endraw %}